Skip to content

bench(recursion): measure the verifier at real query counts, over real blocks#825

Open
MauroToscano wants to merge 3 commits into
mainfrom
bench/recursion-full-queries
Open

bench(recursion): measure the verifier at real query counts, over real blocks#825
MauroToscano wants to merge 3 commits into
mainfrom
bench/recursion-full-queries

Conversation

@MauroToscano

Copy link
Copy Markdown
Contributor

What

The recursion cycle benchmark previously measured one regime: MIN_PROOF_OPTIONS (blowup=2, 1 FRI query) verifying a proof of the empty program. That's a useful diagnostic floor, but the production verifier runs 219 queries at blowup 2, and the real inner object is a multi-epoch continuation bundle of an actual block. This PR makes the benchmark able to measure the real target.

Presets & guests

  • Preset::Blowup2 (219 q) and Preset::Blowup4 (110 q) — the realistic base-layer regimes — alongside Min/Blowup8; one guest ELF per preset (RECURSION_VERIFIER_PRESETS in the Makefile).
  • New continuation guest feature → recursion-cont-{min,blowup2,blowup4}.elf: verifies a whole ContinuationProof bundle in-VM instead of a monolithic VmProof, so the inner prove is memory-bounded (any block size proves on a bounded-RAM box).
  • verify_continuation_with_roots + continuation_precomputed_commitments: supplied DECODE / page-genesis roots skip the in-VM FFT+Merkle recomputes. Trust model is identical to the monolithic guest: roots are folded into the attestation's program_id; the binding is the consumer's recompute+compare.

Benchmark plumbing

  • test_dump_recursion_input env knobs: RECURSION_DUMP_PRESET, RECURSION_DUMP_INNER_ELF/INPUT (any inner program), RECURSION_DUMP_EPOCH_LOG2 (continuation mode).
  • bench_recursion_cycles.sh: preset-aware dump (fails loudly for refs predating it), --release test invocations, regime labels; /bench-verify now posts min + blowup2 + blowup4 tables (builds cached; each extra regime ≈ a blob dump + one ~1–2 min execute per ref).
  • New bench_recursion_scaling.sh: block-size × preset ladder over committed ethrex fixtures (1/4/8/16 transfers, ~13–30 KB each, un-ignored explicitly); preset-major sweep so the headline curve completes first.
  • MAX_PRIVATE_INPUT_SIZE 64 MiB → 512 MiB: the constant is documented as sized to pass "a whole VmProof" to the verifier guest, and real proofs outgrew it (ethrex_20 at production options ≈ 231 MiB monolithic; bundles carry one proof per epoch). Nothing is mapped above the region until the stack, so the layout is unaffected. The pinned test_max_private_input_pages_is_tight value moves 257 → 2049 accordingly.

Measured (ladder, epoch = 2^21, deterministic guest cycles)

txs inner cycles epochs blowup2 (219 q) blowup4 (110 q) min (1 q)
1 1.82M 1 10.49B 6.13B 1.58B
4 3.84M 2 18.98B 10.53B 1.73B
8 6.42M 4 34.39B 18.51B 2.04B
16 11.62M 6 (running) (running) 2.38B

Fits: blowup2 ≈ 2.6B + 8.0B/epoch, blowup4 ≈ 1.7B + 4.4B/epoch, min ≈ 1.42B + 0.16B/epoch. ~98% of the blowup2 per-epoch cost is the 219 queries' openings (~36M cycles/query/epoch — of which the keccak accelerator calls are only ~7K cycles, i.e. the cost is VM scaffolding around the crypto, not the hashing). Net: naive in-VM recursion expands work ~4,000× per layer at current parameters; the empty-program 1-query number under-measured the real verifier cost ~180×.

Notes

  • The old single-query benchmark still runs (regime-labeled) — deltas on it remain a cheap regression signal.
  • The ethrex/continuation regime is deliberately not wired into CI yet; bench_recursion_scaling.sh runs it locally/on the bench box.
  • blowup8 has no continuation guest on purpose (final-wrap parameters, not a base-layer regime).

@MauroToscano

Copy link
Copy Markdown
Contributor Author

Final ladder cells landed (epoch=2^21, deterministic guest cycles):

txs inner cycles epochs blowup2 (219 q) blowup4 (110 q) min (1 q)
1 1.82M 1 10.49B 6.13B 1.58B
4 3.84M 2 18.98B 10.53B 1.73B
8 6.42M 4 34.39B 18.51B 2.04B
16 11.62M 6 52.06B 27.68B 2.38B

Both 16tx cells match the linear fits from the first three points to within 2% (predicted ~51B / ~28B): blowup2 ≈ 2.6B + 8.0B/epoch, blowup4 ≈ 1.7B + 4.4B/epoch. The per-epoch verify cost vs the 2.1M-cycle epoch size puts naive in-VM recursion at ~3,800× expansion per layer at blowup2.

@MauroToscano MauroToscano force-pushed the bench/recursion-full-queries branch from ed9ae73 to 42f98e4 Compare July 15, 2026 19:34
@MauroToscano

Copy link
Copy Markdown
Contributor Author

Rebased onto main now that #769 (rkyv in-place verification) landed — the PR is a single commit on top of the squash, with the continuation guest input ported to the LVMR-prefixed rkyv wire format (ContinuationGuestInput + verify_continuation_and_attest_blob: bytecheck + one rkyv::deserialize pass; zero-copy epoch verify over the archived bundle is noted follow-up).

Re-measured the blowup2 ladder on the rkyv base (identical parameters, CUDA-proved fixtures):

txs epochs pre-#769 post-#769 Δ
1 1 10.49B 7.58B −27.8%
4 2 18.98B 13.24B −30.3%
8 4 34.39B 23.75B −30.9%
16 6 52.06B 35.73B −31.4%

Per-epoch marginal cost dropped ~8.0B → ~5.6B (per-query ~36M → ~25M cycles). Keccak counts are unchanged (1,721,218 vs 1,721,282 at 1tx) — the win is deserialization/traversal removal, as designed. Both host roundtrip tests pass in release on the rebased tree.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Benchmark Results for modified programs 🚀

Command Mean [ms] Min [ms] Max [ms] Relative
head hashmap 77.3 ± 1.4 75.3 78.6 1.00

…l blocks

The recursion cycle benchmark only measured MIN_PROOF_OPTIONS (blowup=2,
1 FRI query) verifying a proof of the empty program - a diagnostic floor,
not the production verifier cost (blowup=2 at 128-bit is 219 queries, and
real inner proofs are multi-epoch continuation bundles of real blocks).

Presets and guests:
- Preset::Blowup2 (219 q) / Blowup4 (110 q) - the realistic base-layer
  regimes - alongside the existing Min/Blowup8; one guest ELF per preset.
- A 'continuation' guest feature: verify a whole ContinuationProof bundle
  in-VM (recursion-cont-<preset>.elf) instead of a monolithic VmProof, so
  the inner prove is memory-bounded via epochs at any block size. The
  bundle rides the same LVMR-prefixed rkyv wire format as the monolithic
  input (ContinuationGuestInput + verify_continuation_and_attest_blob:
  bytecheck + one rkyv::deserialize pass; zero-copy epoch verify over the
  archived bundle is follow-up).
- verify_continuation_with_roots + continuation_precomputed_commitments:
  supplied DECODE/page-genesis roots skip the in-VM FFT+Merkle recomputes;
  binding stays with the attestation fold + consumer recompute, exactly
  like the monolithic guest path.

Benchmark plumbing:
- test_dump_recursion_input gains env knobs: RECURSION_DUMP_PRESET,
  RECURSION_DUMP_INNER_ELF/INPUT (any inner program, e.g. ethrex blocks),
  RECURSION_DUMP_EPOCH_LOG2 (continuation mode).
- scripts/bench_recursion_cycles.sh: preset-aware dump (fails loudly on
  refs that predate it), --release test invocations, per-preset regime
  labels; /bench-verify now posts min + blowup2 + blowup4 tables.
- scripts/bench_recursion_scaling.sh: block-size x preset ladder over the
  committed ethrex fixtures (1/4/8/16 transfers), preset-major so the
  headline curve completes first; DUMP_FEATURES=cuda for GPU boxes.
- MAX_PRIVATE_INPUT_SIZE 64 MiB -> 512 MiB: real proofs outgrew the cap
  the constant was sized for (ethrex_20 at production options is ~231 MiB
  monolithic; continuation bundles carry one proof per epoch). Nothing is
  mapped above the region until the stack, so the layout is unaffected.

Measured (deterministic guest cycles, epoch=2^21, blowup2): verifying an
ethrex block costs ~8B cycles/epoch pre-rkyv and ~5.6B post-rkyv (#769,
-30%), ~98% of it in the 219 queries' openings - naive in-VM recursion
expands work ~2,700-3,800x per layer.
The 64 MiB -> 512 MiB bump this branch claims only ever landed on the
executor half. `executor::vm::memory::MAX_PRIVATE_INPUT_SIZE` is 512 MiB,
but the guest-side twin here stayed at 64 MiB, so
`get_private_input_slice` clamped `len.min(64 MiB)` and handed the guest
the first 64 MiB of any larger blob. A real recursion input (4tx/blowup2
= 143,004,640 bytes) arrived truncated, rkyv rejected the fragment, and
the recursion guest surfaced it as an opaque Panic("PANICKED") ~1s in.

That made the ladder in `scripts/bench_recursion_scaling.sh` unrunnable
on this branch: its own header example (blob=145080513 with a real cycle
count) is only reachable with the guest clamp at 512 MiB, so the value
was 512 MiB when the table was measured and the edit was lost before
commit. The published numbers are unaffected -- re-measuring 4tx/blowup2
at a8a3d87 reproduces 13.36B cycles against the recorded 13.24B at
c30ffe5 (+0.94%, i.e. #815's own cost).

Soundness is unchanged: the clamp exists to bound a forged length prefix,
and it still bounds it -- now at the same cap the host enforces in
`Memory::store_private_inputs`, which is what makes an honest prefix
always `<=` the bound.
@Oppen Oppen force-pushed the bench/recursion-full-queries branch from 5bac9f0 to 59053bb Compare July 16, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant