Skip the IR pre-capture on the riscv64 guest - #949
Conversation
|
/bench-verify |
|
⏳ Benchmark started on the bench server. Two verifier arms (monolithic + continuations over an ethrex 20-tx block), then the recursion-guest cycle comparison, which adds guest builds on top — longer on a cold runner. The bench server is occupied until it finishes. |
Verifier benchmark —
|
| Metric | main | PR | Δ |
|---|---|---|---|
| Verify time (ABBA, 20 pairs) | 2.558s | 2.559s | +0.03% ⚪ |
| Proof size (exact, 1 reading) | 115.67 MiB | 115.67 MiB | +0.00% ⚪ |
pairs: 20 mean A (PR): 2.559s mean B (main): 2.558s
[parametric] paired-t mean +0.03% sd 1.07% se 0.24%
95% CI: [-0.47%, +0.53%] (t df=19 = 2.093)
[robust] median +0.18% Wilcoxon W+=111 W-=99 p(exact)=0.8408 (z=+0.21)
run-to-run jitter: A CV 0.74% B CV 0.61% (lower = steadier)
within-session drift: -0.59% over the run, 1st->2nd half -0.31%
⚪ INCONCLUSIVE — effect not separable from 0 at n=20 (point estimate ~+0.18%). Add pairs to resolve.
ethrex 20-tx block · continuations, epoch 2^20 (4 epochs) · blowup=2, 219 queries
| Metric | main | PR | Δ |
|---|---|---|---|
| Verify time (ABBA, 8 pairs) | 3.974s | 3.967s | -0.18% ⚪ |
| Proof size (exact, 1 reading) | 231.37 MiB | 231.37 MiB | +0.00% ⚪ |
pairs: 8 mean A (PR): 3.967s mean B (main): 3.974s
[parametric] paired-t mean -0.18% sd 0.43% se 0.15%
95% CI: [-0.54%, +0.18%] (t df=7 = 2.365)
[robust] median -0.09% Wilcoxon W+=11 W-=25 p(exact)=0.3828 (z=-0.91)
run-to-run jitter: A CV 0.29% B CV 0.46% (lower = steadier)
within-session drift: -0.03% over the run, 1st->2nd half -0.04%
⚪ INCONCLUSIVE — effect not separable from 0 at n=8 (point estimate ~-0.09%). Add pairs to resolve.
Verify-time rows only: drift-free interleaved A/B/B/A, with paired-t and exact Wilcoxon — trust the verdict when the two agree. Proof sizes are single exact readings (no averaging). - = PR faster.
Recursion guest cycles — verifier running INSIDE the VM (main vs PR)
empty program · monolithic · blowup=2, 1 query (diagnostic — NOT a real verifier cost)
Single exact reading per ref — no ABBA: guest cycles are deterministic for a fixed
(guest ELF, input blob), so there is no machine drift to cancel.
| Metric | main | PR | Δ |
|---|---|---|---|
| Guest cycles | 331.6M | 32.1M | -299.5M (-90.31%) |
| Keccak calls | 3029 | 3029 | 0 |
baseline origin/main 884eb45780 guest=recursion-min.elf
PR 665afe6f44e9fe3858e47483d38ff0fd9e244d8a 665afe6f44 guest=recursion-min.elf
note: cycles reproduce to ~±100k (build codegen + proof nondeterminism);
treat sub-100k deltas as noise, not signal.
raw (exact integer counts)
ref_b_sha=884eb45780016f33b729051a1a70c8abe9a511a6 ref_b_elf=recursion-min.elf ref_b_cycles=331649890 ref_b_keccak=3029 ref_b_execute_wall_s=9
ref_a_sha=665afe6f44e9fe3858e47483d38ff0fd9e244d8a ref_a_elf=recursion-min.elf ref_a_cycles=32138143 ref_a_keccak=3029 ref_a_execute_wall_s=0
delta_cycles=-299511747 delta_keccak=0
ethrex 20-tx block · continuations, epoch 2^21 (2 epochs) · blowup=2, 219 queries (128-bit)
Single exact reading per ref — no ABBA: guest cycles are deterministic for a fixed
(guest ELF, input blob), so there is no machine drift to cancel.
| Metric | main | PR | Δ |
|---|---|---|---|
| Guest cycles | 2279.3M | 1973.7M | -305.6M (-13.41%) |
| Keccak calls | 3534145 | 3534145 | 0 |
baseline origin/main 884eb45780 guest=recursion-cont-blowup2.elf
PR 665afe6f44e9fe3858e47483d38ff0fd9e244d8a 665afe6f44 guest=recursion-cont-blowup2.elf
note: cycles reproduce to ~±100k (build codegen + proof nondeterminism);
treat sub-100k deltas as noise, not signal.
raw (exact integer counts)
ref_b_sha=884eb45780016f33b729051a1a70c8abe9a511a6 ref_b_elf=recursion-cont-blowup2.elf ref_b_cycles=2279309978 ref_b_keccak=3534145 ref_b_execute_wall_s=37
ref_a_sha=665afe6f44e9fe3858e47483d38ff0fd9e244d8a ref_a_elf=recursion-cont-blowup2.elf ref_a_cycles=1973696133 ref_a_keccak=3534145 ref_a_execute_wall_s=31
delta_cycles=-305613845 delta_keccak=0
What
prover/src/test_utils.rs:674, insidebuild_air(), gated out of the guest:The gate is on target architecture, not on a feature, so the host compiles identically and the prover, CUDA and host-verify paths are untouched. Only the riscv64 guest stops building an IR it never reads.
Why
build_airpre-captures the constraint IR so every AIR clone carries it. That is right for the prover — the GPU lowering and the CPU interpreter both need it, and capturing once per prototype beats rebuilding per instance. But the in-VM verifier reaches the samebuild_airthroughVmAirs::new, builds the IR of all 28 AIRs, and never reads it. The capture hash-conses, so in the recursion guest that cost is not latency but trace rows: ~300M guest cycles, 13.4% of the verifier on a real block.traits.rs:274already forbids the verify/recursion path from callingconstraint_program(), and the comment is accurate —verifynever did.build_airdid it on verify's behalf, one step earlier.Grep does not find this — the flamegraph does
The only non-test caller of
constraint_program()is the CUDA arm inevaluator.rs:334, and in the guest the linker had merged the symbol withtest_utils::create_keccak_rnd_air, which compiles to identical code.cli execute <elf> --private-input <blob> --flamegraph <file> --cycleshas the real stack:Why a gate, and why on the target arch
A gate rather than a removal, because the pre-capture is load-bearing on the host: without it the GPU lowering and the interpreter force an IR build per AIR instance rather than once per prototype.
Target arch rather than a feature, because a feature has to be remembered at every guest build site and can fail in the other direction — silently dropping the capture on the host and regressing the prover. The target check cannot misfire: CUDA never compiles for riscv64.
build_airitself is left alone. It is the single construction path for all 28 AIRs in both prover and verifier, so splitting it per-role is a much larger change for the same result.Measured — guest cycles
The
/bench-verifycomment below has the two regimes CI runs: the ethrex 20-tx block at 219 queries, -13.41%, and the 1-query diagnostic. The point it does not run is the one in between, empty program at 219 queries: 909,346,186 -> 608,969,654 = -33.03%, keccak 347,783 -> 347,783.All three carry a keccak delta of exactly zero, so no hashing, Merkle or transcript work was removed. The saving is query-invariant — the removed work runs once, before the first query opens, so only the denominator moves.
Measured — proving the recursion step (empty/219q, shared blob, epoch 2^22)
No CI workflow measures this, so it was run by hand with the same blob on both arms.
217 -> 146 epochs, 2892.689s -> 2035.646s = -29.63%, proof 17.79 -> 11.77 GB = -33.83%, peak RSS 92.2 -> 82.5 GiB.
Sublinear in cycles (-33.03% cycles gives -29.63% time) because the removed work was cheap per cycle:
HashMapinserts make sparse traces, so per-epoch cost rises 13.33s -> 13.94s as only the keccak/Merkle-dense epochs are left.Soundness
The call discarded its result (
let _ =) and only filled aOnceLock, so it cannot change any evaluated value.verifier.rsnever mentionsconstraint_program— it evaluates throughconstraints_meta(). The trait default isunimplemented!(), so a verify path that ever needed the IR would panic loudly rather than skip a check silently. And the keccak deltas above are exactly zero, over 3.5M calls on the real block.No regression on the paths this does not touch
Beyond the two verify arms in the
/bench-verifycomment — both inconclusive, proof size +0.00% on each:/benchreal-block prove is +0.26% (medians of 3, ranges overlap), and on the CUDA prover path a 14-pair ABBA on a rented RTX 5090 shows no change either.