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
|
Reviewed this (mechanism checks out — no riscv64-reachable caller of The two cited percentages can't both be right. The PR body says "~300M guest cycles, 13.4% of the verifier on a real block"; the code comment says "~9.7% of the recursion guest's cycles on a real block." The cycle harness counts total guest cycles, so there's no larger denominator inside the measured run to get from one to the other — 9.7% would need a denominator ~1.38× the whole run. The CI-posted The 13.4% is measured at 2 epochs and shrinks with epoch count. Small one while in there: "the prover's GPU lowering and interpreter paths" over-claims — the CPU IR interpreter ( None of this affects correctness or the recommendation to merge — |
|
Body and comment updated. One deviation: rather than correcting the figure in the comment I dropped it, for exactly the reason in your second point — a number that reads 13.4% at 2 epochs and 3% at 10 belongs in the body, not the source. What went in instead is measured: across the three arms the total moves 6.87× (331.6M → 2279.3M) while the saving moves 2% (299.5M, 300.4M, 305.6M). "interpreter" gone in all three places.
|
What
prover/src/test_utils.rs:677, 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 CUDA composition arm needs 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: ~305.6M guest cycles, 13.4% of the verifier on the 20-tx block at 2 epochs.The prototype cache is process-wide and one guest run verifies the whole continuation bundle, so that saving is one-time per run: the absolute number is the invariant, and the share shrinks as the epoch count grows.
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 CUDA composition arm forces 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.
And the removed work is a fixed one-time cost rather than a proportion: across the three arms the total moves 6.87× (331.6M -> 2279.3M) while the saving moves 2% (299.5M, 300.4M, 305.6M). It runs once, before the first query opens, so only the denominator changes. The 6.1M spread is the continuation config touching a couple more AIR prototypes than the monolithic one.
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.