engine-cuda: native CUDA kernel behind MinerEngine #3
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #1. Largest single lever identified in the audit. Estimated 1.5x to 2.5x on a 5090 over the current 368 MH/s; the estimate is from instruction counts and needs the harness in the benchmark issue to confirm.
Why WGSL is the ceiling
The PoW is two Poseidon2 permutations per nonce over Goldilocks (
p = 2^64 - 2^32 + 1), roughly 1,500 field multiplies per nonce, and nothing else. WGSL has no 64x64 to 128-bit multiply and no multiply-high.gf64_mulincrates/engine-gpu/src/kernels/mining_u64.wgsltherefore splits both operands into 32-bit halves, forms four 64-bit partial products, and reconstructs carries with compare-and-select. Each of those four partials is itself a 64-bit multiply as far as the driver's compiler knows, so it becomes several IMAD.WIDE instructions. The net is on the order of 30 to 40 SASS instructions per field multiply.In CUDA the same multiply is
__umul64hiplus a plain 64-bit multiply, or amad.lo.cc.u32/madc.hi.u32chain in inline PTX, followed by the same2^64 = 2^32 - 1reduction. That is roughly 12 to 15 instructions. The permutation is otherwise adds and the same reduction, so the multiply count is the whole story.Secondary CUDA-only wins, each small: round constants in
__constant__memory instead of whatever naga emits forconstarrays indexed by loop variable; explicit#pragma unrollcontrol; PTXslct/predication instead of WGSLselecton u64, which naga may lower to a branch.Design
New crate
crates/engine-cuda. ImplementsMinerEnginefromengine-cpu. Nothing inengine-gpuchanges.cudarc(pure Rust, dlopenslibcuda, no build-time CUDA toolkit needed for the Rust side). The kernel is compiled to PTX bynvccinbuild.rswhenCUDA_HOMEis set, else the crate builds with the PTX checked in as a fallback artifact for the CI image. Thecuda-builder.cfcontainer origin already ships is the build environment.pow_core::mining_midstate. Same lazy second squeeze. Keeping the contract identical means the parity tests and the batch loop are shared, not duplicated.build.rsemits the round constants and MDS diagonal fromqp-poseidon-constantsinto a generated header. Origin's WGSL hand-copies them; if the hash ever changes at origin, our CUDA kernel updates by dependency bump. This is the coupling rule from #1 applied to the kernel.resolve_gpu_configurationinminer-servicetriesengine-cudafirst when--gpu-engine auto|cudaandlibcudaloads, falls back toengine-gpuotherwise. The flag defaults toauto. Against an origin node nothing changes on the wire.CudaDeviceper card, one stream per worker, same thread-local worker-to-device assignment pattern asengine-gpusoWorkerPoolneeds no change.cuMemcpyDtoHAsyncpluscuEventRecordon stream, poll the event for batch N while batch N+1 runs. The wgpu double-buffering issue does the same for the origin engine; here it is free.Steps
cudarcdevice enumeration, PTX load,MinerEngineimpl that returnsExhausted { 0 }. Wire into resolution behind the flag. Bench harness sees it.mining_u64.wgslto CUDA C, schoolbook multiply kept. Parity passes. Establishes the port is correct before any arithmetic changes.gf64_mul/gf64_sqrwith__umul64hi. Measure.__constant__round constants, unroll tuning, occupancy check with--ptxas-options=-v. Measure each.Each step is a PR with its harness numbers in the description.
Risks
SHADER_INT64parity tests are wgpu-specific. The CUDA parity path callspow_coreon the CPU for the same nonces; the component tests for individual layers do not port and are not needed once end-to-end parity holds at scale (origin's reverted "fuzz GPU/CPU parity at scale" commitb0cfc30is the template).libcuda.sois present wherever the NVIDIA driver is. No toolkit needed at runtime.sm_100/sm_120explicitly; check what the CUDA 12.9 builder image supports.Origin coupling
New crate plus the selection arm and one flag. The generated-constants rule means a hash change at origin is a
Cargo.lockbump here. A mining-protocol change at origin does not touch this crate at all.Origin history has a CUDA engine crate that was removed when wgpu arrived:
crates/engine-gpu-cudaat5a6de31^(deleted in "add wgpu support (#28)"), with.github/workflows/cuda-build.ymland aContainerfilefromd2c39bb("ci: build cuda binaries").Its kernel is not reusable: it was written for the earlier proof of work (512-bit Montgomery CIOS multiply via
__umul64hi, Keccak constants, "G1" bring-up with Poseidon2 on the host). The scaffolding is:build.rs:nvccdiscovery viaNVCC/CUDA_HOME/PATH,CUDA_ARCH=sm_NNnormalised to-arch compute_NN -code sm_NN, cubin plus PTX both embedded with a header preflight that fails the build rather than emit a GPU-broken binary,MINER_NVCC_CCBINandMINER_CUDA_ALLOW_UNSUPPORTED_COMPILERknobs.mul64wide/add64_carryhelpers using__umul64hi, which is exactly step 3 of this issue's plan.scripts/local-cuda-build.sh(still in tree) with the fleet's arch list86 / 89 / 120and CUDA 13.0.Start step 1 from that
build.rsrather than fromcudarc's examples; it already encodes the fleet's build constraints. The arch matrix in the CI issue assumes itsCUDA_ARCHcontract.Steps 1 to 3 landed: #16 (2026-09-03)
Harness on benjy via the
benchworkflow (actions/runs/28), miner paused, 5 x 30 s windows, batch 1M:2.06x on the reference card. On quadbrat's 3060 at 130 W: 37.3 to 59.5 MH/s, 1.59x. Both bit-exact against
pow_coreon every job.What the port does differently from the WGSL: the field multiply is
__umul64hiplus a plain 64-bit multiply and one Goldilocks reduction, and the round loops are unrolled with the constants in__constant__memory. Nothing else; the sponge schedule and host contract are identical, which is why parity held first time.Grid sizing (
MINER_CUDA_THREADS_PER_SM): 8192 and above flat on the 3060, 2048 loses 10%. Default 8192. A 4M batch measured slower than 1M on the 3060 (52.6 vs 59.5), which is the per-thread nonce loop at that grid cap; worth a sweep on the 4090 with the harness before #6 touches batch size for the CUDA engine.Remaining from this issue's plan: step 5 (constant handling and unroll tuning,
--ptxas-options=-voccupancy check), step 6 (pipelined submission; worth at most the host share of a batch, which #9 measures live), and the 5090 number, which needs beast and a manual dispatch.Deployed to both hosts by the merge; validate asserts
kernel="cuda"on device 0.Step 5 landed: #17 (2026-09-03)
Every variant parity-checked on quadbrat, then timed on benjy (RTX 4090, 250 W, miner paused) in interleaved rounds. Full-protocol confirmation by the
benchworkflow: 408.27 MH/s, spread 0.2%, parity 25/25 (actions/runs/33). Origin's wgpu build on the same card and limit: 144.4. 2.83x.gf_add/gf_reduce(inline PTX)Rejected:
__noinline__permute (-13%),-maxrregcount80 and 64 (flat), internal-round unroll 1 or 22 (flat). Code size and occupancy are not the limit on Ada; instruction count per hash is, and the adds were a larger share of it than the audit assumed.Measurement notes for whoever continues: run-to-run noise on benjy is about ±1.5% with the SM clock moving 1935 to 2025 MHz at the cap, so anything under 3% needs interleaved rounds and is not resolvable in a single pass. Ad-hoc runs over ssh do not take the workflow's host lock; do not run them while a
benchjob is measuring on the same host.Still open on this issue: step 6 (pipelined submission; ceiling is the host share of a batch, 1.9% on the 4090 per #9, less now that batches are shorter), the
sm_120codegen question (255 registers with spills on Blackwell against 128 on Ada), and the 5090 number itself. Both of the last two need beast.The 5090 number (2026-09-03, beast, neuron stopped for the run)
Both RTX 5090s at their 400 W floor, driver 580.178.04, binary built on beast from
main(ad66f13), harness with two workers (one per card), parity 12/12 verified against CPU on each engine.Single card alone: 553.7. Interleaved rounds put the host at 1,109 to 1,134 MH/s (clock 2265 to 2317 MHz at the cap).
Checked and settled:
LAIR_INT_UNROLL=1removes the spill but measures 1 to 1.5% slower (1,118 / 1,109 vs 1,134 / 1,122 interleaved). Default stays.The 5090's gain over origin (1.96x) is smaller than the 4090's (2.83x) because origin's wgpu kernel is relatively better on Blackwell, not because the CUDA kernel is worse: per card the CUDA kernel does 554 MH/s at 400 W against 414 on the 4090 at 250 W, which is the same 1.4 MH/s per watt.
Fleet total on the tuned engine, if beast mined: 414 + 81 + 1,109 = about 1.6 GH/s.
Closing: the engine is deployed on every mining host and measured on every card in the fleet.
Step 6 (pipelined submission) is not worth its own issue on this engine: the host share of a batch is under 2% (#9), so it is folded into #5, which stays open as the one pipelining issue for both engines. Constants are generated from
qp-poseidon-constantsat build time, the fat binary carries sm_86/89/120 plus PTX, and every kernel knob remains a-Dmacro reachable throughMINER_NVCC_FLAGSfor the harness.