perf(gpu): grind the proof-of-work nonce on the GPU - #936
Conversation
|
/ai-review |
GPU Benchmark (ABBA) —
|
Codex Code Review
|
Review: GPU proof-of-work grindingI verified the kernel derivation against the host predicate and it checks out: Findings, none blocking correctness of the happy path: Medium
Low
Nits
|
AI ReviewPR #936 · 6 changed files Findings
Status column reflects the verdict from the verifier: deepseek-verifier (openrouter/deepseek/deepseek-v4-pro). AI-001: GPU nonce validity only checked in debug builds
Claim In release builds, an invalid nonce returned by the GPU kernel would be accepted and used in the proof, producing an invalid proof. The GPU result is only validated by a debug_assert!, which is stripped in release. Evidence crypto/stark/src/grinding.rs lines 115-120: after generate_nonce_gpu returns a nonce, the code runs debug_assert!(is_valid_nonce(seed, nonce, grinding_factor), ...). debug_assert! is compiled out in release, so any GPU-side bug (kernel regression, driver/hardware issue) would silently yield an invalid nonce. The check costs only ~2 Keccak hashes versus the ~2^grinding_factor hashes already performed, so there is no performance justification for disabling it in release. Suggested fix Either make the check a real runtime AI-003: Non-atomic read of atomic result for early exit optimization
Claim The early-exit check Evidence Line 172 reads *result directly. The variable is written via atomicMin on line 187. This is a classic data race pattern, though harmless for this optimization. Suggested fix Use atomicLoad or __ldg to read the value, or simply remove the optimization since the grid-stride loop already bounds work. If kept, annotate with // NOLINT or similar to suppress race detectors. AI-005: Repeated device allocation in generate_nonce_gpu loop
Claim A new result_dev buffer is allocated via clone_htod(&[u64::MAX]) on every loop iteration, causing unnecessary allocation overhead when multiple grid launches are needed (e.g., for high grinding factors). Evidence Line 49: let mut result_dev = stream.clone_htod(&[u64::MAX]).ok()?; inside the loop at line 48. The buffer is only used to receive the atomicMin result and could be allocated once before the loop and reused. Suggested fix Move the result_dev allocation before the loop and reuse it by resetting to u64::MAX each iteration (e.g., via stream.memset or a tiny kernel). Reviewer Lanes
Verification Lanes
Native Codex and Claude reviews run separately and post their own comments. They are not included in this structured provenance report. Discarded candidates (2) — rejected by the verifier
Raw lane outputs, candidates, final issues, and model metrics are uploaded as workflow artifacts. |
7ee4d16 to
0b3ab88
Compare
Grinding (generate_nonce) runs a ~2^grinding_factor parallel Keccak search per table per epoch and is the prover's dominant CPU cost — 64.7% of on-CPU time in a 100tx flamegraph, on the 16 cores while the GPU sits ~66% idle. Add a keccak nonce-search kernel (each thread strides a nonce block, atomicMin keeps the smallest valid nonce), a math-cuda wrapper that searches in expanding blocks from 0, and a stark dispatch that computes the inner hash on the host, validates the device result unconditionally, and falls back to the CPU search on any device miss or invalid nonce. Result-valid: the verifier only checks is_valid_nonce, so any valid nonce works. A device launch is skipped below a minimum grinding factor (tiny factors are faster on the CPU), and LAMBDA_VM_NO_GPU_GRIND forces the CPU path. GPU_GRIND_CALLS counts the dispatches so a silent fallback is caught by the integration test. 100tx e20 (ABBA, same binary): 18.89s -> 13.10s = -30.6%.
0b3ab88 to
5a895bc
Compare
|
/bench-gpu |
|
/bench-gpu |
Route the GPU dispatch and its tests through one inner-hash-to-lanes conversion. The tests built their own copy, so the line the prover actually runs was executed by nothing: swapping it to from_be_bytes would have kept every test green while is_valid_nonce rejected every device nonce at runtime and the search sat on the CPU fallback forever. stark::grinding:: inner_hash_lanes is now the single entry point, which also lets get_inner_hash go back to private. Report that fallback on stderr instead of log::warn. The CLI initialises env_logger with no default filter, so a warn-level line never prints unless RUST_LOG is set — and it is the only signal that the kernel has started returning garbage. The other device-decline paths already use eprintln with a [gpu] prefix. Wrap test-math-cuda in GPU_TEST_TIMEOUT. It was the only one of the five GPU targets without it, and it is Group 1 of gpu_test.sh, so a hang there costs Groups 2-5 as well and a job timeout yields `cancelled`, which skips the run-summary step and leaves no readable output. Document LAMBDA_VM_NO_GPU_GRIND in the profiling README's knob list. Drop the "Parity" framing from the test module: there is nothing to be at parity with, since any valid nonce is acceptable and the CPU's find_any does not agree with itself between runs. What is pinned is validity, plus the search completeness that minimality stands in for — noted as a probe rather than a contract, so a future kernel that deliberately returns any valid nonce relaxes the assertion instead of being treated as broken. Same for the doc on generate_nonce_maybe_gpu, which claimed "smallest" for both arms.
Grinding (generate_nonce) runs a ~2^grinding_factor parallel Keccak search per table per epoch and is the prover's dominant CPU cost — 64.7% of on-CPU time in a 100tx flamegraph, on the 16 cores while the GPU sits ~66% idle.
Add a keccak nonce-search kernel (each thread strides a nonce block, atomicMin keeps the smallest valid nonce), a math-cuda wrapper that searches in expanding blocks from 0, and a stark dispatch that computes the inner hash on the host and falls back to the CPU search on any device miss. Result-valid: the verifier only checks is_valid_nonce, so any valid nonce works. LAMBDA_VM_NO_GPU_GRIND forces the CPU path; below a minimum grinding factor the GPU launch is skipped (tiny factors are faster on the CPU), and GPU_GRIND_CALLS counts the dispatches.
100tx e20 (ABBA, same binary): 18.89s -> 13.10s = -30.6%.