perf: whole-grid batches, early reject on element 0, precomputed nonce direction (+2.9% on beast) #26

Merged
grenade merged 1 commits from perf/scheduling into main 2026-09-13 21:06:33 +00:00
Owner

The scheduling leads left after #23, each measured with three interleaved rounds and parity on every host (quantus/miner#3).

Batches of whole grids (host). The CLI's 1M-nonce batch filled a 5090 eleven waves deep with a half-empty tail wave and paid the launch gap every 1M hashes. Batches are now rounded up to a multiple of the grid (SMs x threads per SM), two grids per batch by default so each thread loops over two nonces: beast +1.6% for one grid, +0.4% more for two; benjy +0.5% for the second grid; quadbrat neutral. A batch is ~10 ms on every card, so a job switch discards ~0.1% of a block interval. MINER_CUDA_BATCH_ALIGN=0 restores the CLI batch, MINER_CUDA_BATCH_WAVES sets the minimum grids per batch.

LAIR_EARLY_REJECT (kernel). The second permutation stops before its last external linear layer; output 0 alone (the hash's top 64 bits) is computed and compared with the target's top words, and only the ~2^-47 of nonces that survive pay for the other eleven outputs and the canonicalisation. +0.6%.

LAIR_NONCE_DIR (host + kernel). The host computes the state after the first external layer and round constant for the batch's first nonce (first_layer_after_absorb, in canonical arithmetic, with a unit test that the matrix column the kernel assumes is [1,1,3,2,2,2,6,4,1,1,3,2]). The kernel derives each nonce's state as that base plus a scalar (the byte-swapped low limb difference) times that column via gf_mul_add, twelve fused multiply-adds instead of the absorb and full layer. Nonces past a carry out of the low limb take the old path. +0.25% on beast, +0.6% on the 4090 and 3060. The uniform block grows from 224 to 320 bytes.

Tried and rejected: 512-thread blocks (-0.4%; the LAIR_TPB / MINER_CUDA_THREADS_PER_BLOCK knob stays for experiments), a smaller grid via MINER_CUDA_THREADS_PER_SM=16384 (-0.6%), a larger one at 65536 (within noise of two waves), and the whole x^7 S-box as one PTX block, which compiles to byte-identical SASS (noted in a comment on gf_sbox).

Measured, final tree vs the deployed #23 build:

host deployed this PR
beast 2x5090 2143 2205 MH/s +2.9% (1103/card vs the pool binary's 1171)
benjy 4090 711 ~718 MH/s +1.0% (nd build, two waves)
quadbrat 3060 140.3 141.2 MH/s +0.6%

Parity: 2000/2000 on beast with the final build, 500/500 and 300/300 on the intermediate builds on beast, benjy and quadbrat. Static: sm_120 21,120 instructions, 110 registers, no spill; sm_86/89 22,968 instructions, 80 registers with an 8-byte spill that did not show on hardware.

Where this leaves the gap to the pool binary: instruction parity, ~6% of cycles per nonce on the 5090 that the profiler attributes to scheduling and the power envelope. No further scalar lever is known; the voltage/frequency curve is the remaining host-side one.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF

The scheduling leads left after #23, each measured with three interleaved rounds and parity on every host (quantus/miner#3). **Batches of whole grids** (host). The CLI's 1M-nonce batch filled a 5090 eleven waves deep with a half-empty tail wave and paid the launch gap every 1M hashes. Batches are now rounded up to a multiple of the grid (SMs x threads per SM), two grids per batch by default so each thread loops over two nonces: beast +1.6% for one grid, +0.4% more for two; benjy +0.5% for the second grid; quadbrat neutral. A batch is ~10 ms on every card, so a job switch discards ~0.1% of a block interval. `MINER_CUDA_BATCH_ALIGN=0` restores the CLI batch, `MINER_CUDA_BATCH_WAVES` sets the minimum grids per batch. **`LAIR_EARLY_REJECT`** (kernel). The second permutation stops before its last external linear layer; output 0 alone (the hash's top 64 bits) is computed and compared with the target's top words, and only the ~2^-47 of nonces that survive pay for the other eleven outputs and the canonicalisation. +0.6%. **`LAIR_NONCE_DIR`** (host + kernel). The host computes the state after the first external layer and round constant for the batch's first nonce (`first_layer_after_absorb`, in canonical arithmetic, with a unit test that the matrix column the kernel assumes is `[1,1,3,2,2,2,6,4,1,1,3,2]`). The kernel derives each nonce's state as that base plus a scalar (the byte-swapped low limb difference) times that column via `gf_mul_add`, twelve fused multiply-adds instead of the absorb and full layer. Nonces past a carry out of the low limb take the old path. +0.25% on beast, +0.6% on the 4090 and 3060. The uniform block grows from 224 to 320 bytes. **Tried and rejected**: 512-thread blocks (-0.4%; the `LAIR_TPB` / `MINER_CUDA_THREADS_PER_BLOCK` knob stays for experiments), a smaller grid via `MINER_CUDA_THREADS_PER_SM=16384` (-0.6%), a larger one at 65536 (within noise of two waves), and the whole x^7 S-box as one PTX block, which compiles to byte-identical SASS (noted in a comment on `gf_sbox`). **Measured**, final tree vs the deployed #23 build: | host | deployed | this PR | | |---|---|---|---| | beast 2x5090 | 2143 | 2205 MH/s | +2.9% (1103/card vs the pool binary's 1171) | | benjy 4090 | 711 | ~718 MH/s | +1.0% (nd build, two waves) | | quadbrat 3060 | 140.3 | 141.2 MH/s | +0.6% | Parity: 2000/2000 on beast with the final build, 500/500 and 300/300 on the intermediate builds on beast, benjy and quadbrat. Static: sm_120 21,120 instructions, 110 registers, no spill; sm_86/89 22,968 instructions, 80 registers with an 8-byte spill that did not show on hardware. Where this leaves the gap to the pool binary: instruction parity, ~6% of cycles per nonce on the 5090 that the profiler attributes to scheduling and the power envelope. No further scalar lever is known; the voltage/frequency curve is the remaining host-side one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF
grenade added 1 commit 2026-09-13 20:58:41 +00:00
engine-cuda: batches of whole grids, early reject on element 0, nonce direction
All checks were successful
ci / fmt (pull_request) Successful in 19s
bench / build (pull_request) Successful in 1m15s
ci / clippy (pull_request) Successful in 1m45s
ci / doc (pull_request) Successful in 1m58s
bench / measure (pull_request) Successful in 2m51s
ci / test (pull_request) Successful in 6m46s
2e6fce162e
Three scheduling changes measured against the #23 kernel, three interleaved
rounds each, parity clean on every host:

- Batches are rounded up to whole grids, two per batch by default
  (MINER_CUDA_BATCH_ALIGN, MINER_CUDA_BATCH_WAVES). The CLI's 1M-nonce
  batch left a half-empty tail wave and paid the launch gap every 1M
  hashes. beast +1.6% for one grid, +0.4% more for two; benjy +0.5% for
  the second grid; quadbrat neutral.
- LAIR_EARLY_REJECT: the second permutation stops before its last linear
  layer, computes output 0 alone (the hash's top 64 bits) and rejects on
  it; only the rare survivors pay for the other eleven outputs. +0.6%.
- LAIR_NONCE_DIR: the host precomputes the state after the first linear
  layer for the batch's first nonce (first_layer_after_absorb, unit test
  against the matrix column), and the kernel derives each nonce's state
  as that plus a scalar times column 7 of the external matrix; nonces
  past a carry out of the low limb take the old path. +0.25% on beast,
  +0.6% on the 4090 and 3060.

Also tried and rejected: 512-thread blocks (-0.4%), a smaller grid via
MINER_CUDA_THREADS_PER_SM=16384 (-0.6%), a larger one (65536, within
noise of two waves), and the whole S-box as one PTX block, which
compiles to byte-identical SASS. The threads-per-block knob
(MINER_CUDA_THREADS_PER_BLOCK with LAIR_TPB) stays for experiments.

beast 2x5090: 2146 -> ~2206 MH/s (+2.8%); benjy 4090: 711 -> ~718;
quadbrat 3060: 140.2 -> 141.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue5ZZm1Hiv5zPnucykKKuF

quantus-bench 2e6fce162e on benjy.hanzalova.internal

worker median MH/s spread windows
0 709.92 0.2% 709.5, 710.3, 709.2, 710.4, 709.9

total median: 709.92 MH/s (batch 1000000, 5 x 30s, engine gpu-cuda)

  • gpu 0 NVIDIA GeForce RTX 4090: driver 580.173.02, limit 250 W, draw 250 W, sm 1905 MHz, mem 810 MHz, 57 C
  • parity: OK (25/25 jobs found solutions, all verified against CPU)
    --- resume miner ---
    active
## quantus-bench 2e6fce162e400a6e096a54bc461b2c461d2d3790 on benjy.hanzalova.internal | worker | median MH/s | spread | windows | | --- | --- | --- | --- | | 0 | 709.92 | 0.2% | 709.5, 710.3, 709.2, 710.4, 709.9 | total median: **709.92 MH/s** (batch 1000000, 5 x 30s, engine gpu-cuda) - gpu 0 NVIDIA GeForce RTX 4090: driver 580.173.02, limit 250 W, draw 250 W, sm 1905 MHz, mem 810 MHz, 57 C - parity: OK (25/25 jobs found solutions, all verified against CPU) --- resume miner --- active
grenade merged commit 17caeabc10 into main 2026-09-13 21:06:33 +00:00
grenade deleted branch perf/scheduling 2026-09-13 21:06:34 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: blackbeard/miner#26