An `asm` block is opaque to nvcc's optimiser, so carry arithmetic written
as inline PTX blocks common-subexpression elimination and strength
reduction across neighbouring field operations. On Blackwell that costs a
quarter of the kernel. On Ada and Ampere the hand-written sequence still
wins, by a similar margin in the other direction, so the default is now
chosen per device pass.
Whole-kernel instructions (ptxas + cuobjdump, CUDA 13.0) and measured
hashrate, three interleaved rounds each:
sm_120 sm_86/89
PTX_CARRY=1 37,432 1127.1 MH/s 37,027 benjy 461.6 quadbrat 85.5
PTX_CARRY=0 27,837 1358.0 MH/s 41,196 benjy 401.5 quadbrat 76.3
sm_86/89 keep the exact configuration they had, so those cards are
unchanged by construction -- identical macro values, identical 37,027
instructions. Parity 60/60 against the CPU on beast.
LAIR_PTX_ACC is now 1 everywhere rather than following LAIR_PTX_CARRY. In
the Acc accumulators the add-with-carry pair is the whole operation, so
there is nothing around it for the optimiser to fold; on sm_120 with the
carry path off, Acc PTX on measured 1333.3 against 1285.8 with it off.
PR #17 measured the PTX carry path as a win on every card. That was true
of the kernel it was written for, which spilled 104 bytes per thread;
once the spills went the trade reversed on Blackwell only.
Found by profiling rather than reading: ncu puts us at 43,700 executed
instructions per nonce against the rival binary's 22,229 on the same
card, a 1.97x ratio that matches the 2.05x throughput gap, with 82% of
the excess on the ALU pipe rather than the multiply pipe. This lands
35,785. The remaining gap is still ALU: 26,586 against their 14,413,
while the multiply pipe is now close to parity at 8,405 against 7,030.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ytCkttecH9SNTUEw8VT9D
The three-multiply squaring is a real algebraic saving and a measured
loss. Implemented, verified bit-exact against __int128 over 40M random
values plus limb and power-of-two boundaries, then benchmarked on beast:
1072.9 vs 1137.7 MH/s over three interleaved rounds, 5.7% slower.
It trades 6.7% of the kernel's widening multiplies for 11% more
instructions -- 5 IMAD.WIDE in 56 instructions against the general
product's 6 in 48 -- and four formulations (condition-code carry, compare
carry, two mad.wide.u32 variants) all compiled to the same 5/56, so that
is the floor rather than a tuning failure.
Taken with the LAIR_INT_UNROLL result, the picture is consistent: this
kernel is bound by instruction issue and by the cards' power envelope,
not by multiply throughput or occupancy. Re-expressing the same work
cannot win; only removing work can. Left as a comment so the next person
does not spend the afternoon rediscovering it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ytCkttecH9SNTUEw8VT9D
The kernel copied midstate, target and the nonce base into per-thread
register arrays and held all 56 registers live across the whole nonce
loop, which is exactly what the permutation's own working set needed.
ptxas spilled 104 bytes per thread on sm_120 and 44 on sm_86/89; the SASS
carried 26 LDL + 26 STL inside the hot loop.
They are launch-uniform, so they now travel as a by-value MiningUniforms
parameter. Parameters live in constant memory -- broadcast and cached, no
register residency -- and arrive with the launch, so three memcpy_htod
calls per batch are gone with them. Only the low eight nonce limbs are
materialised now; on a hit the high half is written from the parameter
bank. 128 -> 80/80/106 registers on sm_86/89/120, zero spill everywhere.
Both sides assert the 224-byte layout, so a mismatch is a build error
rather than a silent mining bug.
Measured A/B on the fleet, same session, same silicon, parity verified
against the CPU (25/25 on quadbrat and benjy, 40/40 on beast):
quadbrat 3060 83.45 -> 84.60 MH/s +1.38%
benjy 4090 457.87 -> 461.77 MH/s +0.85%
beast 2x5090 1133.97 -> 1138.79 MH/s +0.43%
fleet 1675.29 -> 1685.16 MH/s +0.59%
Not taken: a per-arch LAIR_INT_UNROLL of 1 on sm_120. It is better by
every static measure -- 78 registers instead of 106, three resident
blocks per SM instead of two, no spill either way -- and measured 0.92%
slower on beast over three interleaved rounds. These cards are
power-bound, not latency-bound: the extra warps buy power draw and the
card clocks down to pay for it. Benjy shows the same thing from the other
side, delivering +0.85% at a lower clock (2085 vs 2160 MHz), +4.4% per
MHz. Occupancy is not the lever here; work per hash is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ytCkttecH9SNTUEw8VT9D