Skip to content

Feat/batched fri per epoch#768

Draft
diegokingston wants to merge 45 commits into
mainfrom
feat/batched-fri-per-epoch
Draft

Feat/batched fri per epoch#768
diegokingston wants to merge 45 commits into
mainfrom
feat/batched-fri-per-epoch

Conversation

@diegokingston

Copy link
Copy Markdown
Collaborator

No description provided.

Round 1 now commits all tables' main-split LDE matrices into ONE mixed-height
MixedMmcs and absorbs a single root (before the shared LogUp challenges),
replacing the per-table main-root absorption. Single-table roots are byte-
identical to the old per-table row-pair tree, so single-table proofs are
unchanged; only multi-table transcripts change, so multi-table prove->verify
tests are #[ignore]'d until the verifier is updated (Scope B Task 7). The MMCS
is built transiently for the root (Task 5 will thread it into round 4 and drop
the per-table trees).
Absorb ONE mixed-height MMCS root over every aux-carrying table's LDE into
the shared transcript (after Phase B LogUp challenges, before forking),
replacing the per-fork per-table aux root absorptions. Mirrors the main-trace
batched commit. Transient MMCS (root only); Round 4 still opens per-table aux
trees. Single-table roundtrips byte-identical (131 passed / 17 ignored).
Drop the per-table transcript forks. multi_prove now runs one linear
transcript: bus contributions bind, then per-table beta (sequential), then
round 2 composition built in parallel and committed as ONE mixed-height MMCS
root, then round 3 (z, OOD) and round 4 (FRI) per table sequentially with
PER-TABLE z. prove_rounds_2_to_4 is inlined and removed.

Round 4 is still per-table (per-table FRI + per-table StarkProof/MultiProof) —
the single batched FRI and shard proof format are the next step. Multi-table
verify stays gated to the verifier task; single-table roundtrips remain
byte-identical (131 passed / 17 ignored, clippy clean) because for one table
the linear transcript equals the old fork and the single-matrix composition
MMCS root equals the per-table composition root.
Sample a single z for the whole epoch instead of one per table, against the
tallest table's domain. Since every shorter table's LDE and trace domain is a
subgroup of the tallest's, z out-of-domain for the tallest is out-of-domain
for all. Cleaner and simpler to mirror in the verifier; byte-identical for a
single table (131 passed / 17 ignored, clippy clean).
Add the unified-shard proof types: BatchedMultiProof (shared main/aux/composition
MMCS roots + one batched FRI + per-query MixedOpenings + per-table OOD data),
BatchedQueryOpening (one shared auth path per phase per query), BatchedTableData.
Make MixedOpening serde-serializable. Additive/pub — compiles clean, no behavior
change yet; the prover (multi_prove_batched) and verifier wire these next.
Compute every table's round-3 OOD and absorb it before starting round 4,
instead of interleaving round 3/4 per table. Creates the shared 'after rounds
1-3' boundary the batched path will reuse, and is required for the batched
round 4 (one FRI over all tables needs all OOD bound first). Reference
MultiProof path unchanged in behavior; single-table byte-identical (131/0/17).
Extract rounds 1-3 into prove_rounds_1_to_3 (returns round1s/round2s/round3s,
shared z, and the three KEPT MMCS trees + per-table lde heights). multi_prove
(reference MultiProof path) reuses it unchanged. New multi_prove_batched runs
the batched round 4:
  gamma (one shared DEEP challenge) -> per-table DEEP codewords (bit-reversed)
  -> absorb height histogram -> alpha -> combine_by_height -> batched_commit_phase
  (fold-and-inject FRI) -> grinding -> query indices vs the tallest domain
  -> per-query open_batch on main/aux/composition MMCS (one shared path each) +
  per-preprocessed-table precomputed openings -> BatchedMultiProof.
Reuses Scope-A fri::batched primitives. Reference path byte-identical
(131/0/17, clippy clean). No verifier yet, so no batched roundtrip.

Also: module-level allow(private_interfaces) — surfacing the rounds-1-3 bundle
through a pub trait method trips the lint across the whole internal trait; the
round types are never nameable across the crate boundary.
Mirror multi_prove_batched: one linear transcript, one shared OOD point z,
and one fold-and-inject FRI over the height-combined per-table DEEP codewords,
with all tables opened per query from the three shared mixed-height MMCS trees.

- batched_multi_verify + batched_synthetic_table_proof (reuses step_2 and
  reconstruct_deep_composition_poly_evaluation per table via a lightweight
  synthetic StarkProof carrying only the OOD/public fields those read).
- Transcript replay: precomputed roots -> batched main root -> LogUp
  challenges -> batched aux root -> bus contribs -> per-table betas ->
  batched composition root -> shared z (tallest domain) -> per-table OOD ->
  gamma -> derive_batched_fri_challenges (histogram, alpha, betas, iotas).
- MMCS verify_batch per phase with AIR-intrinsic width binding: main-split
  width = trace_columns - num_aux - num_precomputed (trace_layout().0 is a
  logical figure for step-packed AIRs, not the physical column count).
- Fold-and-inject query check inverts batched_commit_phase exactly: initial
  fold of the tallest (uncommitted) layer, then per layer inject the tables
  entering at that height (beta^2 * combined[h]) before verifying/folding.

Converts all 17 Task-7-gated multi-table tests to the batched path
(multi_prove_batched_ram + batched_multi_verify); 148 pass, 0 ignored.
11 tamper tests + 1 valid-anchor over a bus-balanced 3-table padding epoch,
exercising every batched_multi_verify path: main/aux/composition MMCS auth,
MMCS width binding, fold-and-inject terminal + layer-root + layer-sym checks,
composition-OOD (step 2), query-count guard, grinding nonce, bus balance.
160 stark lib tests pass; new file clippy-clean.
…c path

Point the public prove/verify at the batched STARK path so the existing
server benchmarks measure it (prove/verify time + proof size), and to restore
multi-table correctness — the linear-transcript refactor left the reference
multi_prove -> multi_verify broken for multi-table (verifier still forked).

- VmProof.proof: MultiProof -> BatchedMultiProof (serde-compatible; external
  consumers deserialize opaquely).
- prove(): Prover::multi_prove -> multi_prove_batched.
- verify_with_options(): batched_multi_verify + per_table.len() + batched
  COMMIT-bus-balance replay (replay_transcript_phase_a_batched: precomputed
  roots + single main_root + shared LogUp z/alpha).
- test_utils: multi_prove_batched_ram. Minimal prove/verify test helpers +
  lt_bus completeness helper switched to batched (validated: the 4 lt_bus
  completeness tests, which FAIL on the reference path, PASS on batched).

Scope: exploratory prototype for measurement. Continuation (continuation.rs)
stays on the reference path — its cross-epoch L2G binding reads PER-TABLE main
roots that the unified shard replaces with one MMCS root (a real design
question, deferred). Reference multi_prove/multi_verify kept alive for it.
Switch two positive multi-table roundtrips to the batched path as end-to-end
validation of the default MMCS prover/verifier:
- lt_bus completeness (LT sender/receiver): 4 tests that FAIL on the reference
  path now pass on batched.
- bitwise honest (true preprocessed table via with_preprocessed + hardcoded
  commitment): exercises precomputed-root check + precomputed openings + DEEP
  base = precomputed ++ main-split — the preprocessed path the real VM uses.
Together with the stark-crate roundtrip/soundness/mixed-height/different-blowup
coverage, every constituent path of the monolithic batched prove/verify is
validated locally (full 14-table ELF run needs the server toolchain).
@diegokingston

Copy link
Copy Markdown
Collaborator Author

/bench

Resolves conflicts from #748 (feat(cuda): keep LDE and Merkle trees
resident on the GPU) and #726 (profiling markers), which both landed on
main after this branch forked.

Conflicts:
- crypto/stark/src/prover.rs: both sides heavily rewrote `multi_prove`.
  #748 restructured rounds 1-4 into a peak-VRAM-bounded chunked loop
  (`peak_chunks` + per-table `prove_rounds_2_to_4`) with resident-GPU
  paths; this branch replaced the same region with the batched
  unified-shard path (linear transcript, batched composition MMCS,
  single FRI). A line merge Frankensteined the two, so `prover.rs` is
  taken from this branch wholesale: the batched rewrite subsumes the
  chunked per-table structure, and GPU residency for the batched path is
  deferred to T9 (GPU batched MMCS + FRI). This drops #748's in-
  `multi_prove` GPU-residency ONLY; its cuda-gated changes to other
  files (gpu_lde, math-cuda, trace) remain from the auto-merge.
- prover/src/lib.rs: kept this branch's `multi_prove_batched` /
  `batched_multi_verify` calls and folded in main's orthogonal
  `instruments::span("proving")` and `profile_markers::step_marker`.

Verified (non-cuda): `cargo check` clean on stark + lambda-vm-prover;
17 batched stark tests, serialized multi-table roundtrip, 18 lt_bus
real-VM-table tests, and the honest-bitwise preprocessed path all pass.

NOTE: cuda-build integration between the batched path and #748's
resident-GPU API is deferred (T9); non-cuda is the benchmarked config.
@diegokingston

Copy link
Copy Markdown
Collaborator Author

/bench

Resolve conflicts from main's #764 (single-source constraints) refactor:

- verifier.rs: keep the batched imports (BatchedMultiProof, BatchedTableData);
  drop PackingShifts. #764 changed TransitionEvaluationContext::new_verifier
  from 6 args to 4 (removed periodic_values + packing_shifts, now handled
  internally). The batched verify path reuses the same shared per-table
  transition helper, so this applies to both verify paths.
- air_tests.rs: accept main's deletion of test_multi_prove_different_airs.
  The test predates this PR (the PR only ported it to the batched API); #764
  removed it together with its bit_flags example dependency. No bit_flags
  references remain.
- prover.rs: drop the now-dead IntoParallelRefIterator import (this PR had
  already removed its last .par_iter() call but left the import).

Validated: cargo check clean (no warnings); stark lib suite 192/192 pass,
including the batched soundness suite and logup_all_packing_variants.
Merging main surfaced breaks in code my default-feature validation never
compiled:

- disk-spill: `prove_rounds_1_to_3` takes `air_trace_pairs: &mut Vec<_>`, but
  two disk-spill call sites reborrowed it as `&mut air_trace_pairs` (a
  `&mut &mut Vec`, needing a `mut` binding). `par_try_for_each_mut` takes
  `&mut [T]`, so pass `air_trace_pairs` directly (implicit reborrow).
- cuda: main changed `try_build_comp_poly_tree_gpu` to return
  `(host_tree, dev_tree)` and `try_inv_denoms_dev_with_stream` to take a 4th
  `bound_stream` arg; the auto-merge kept main's signatures but the PR's
  callers. Map the GPU tuple to just the host tree (this batched path doesn't
  use the device tree) and pass `lde_trace.bound_stream()` to match main.
- Lint: `cargo fmt` (much of the branch was never formatted).

Verified: cargo check clean on default, `disk-spill`, `cuda`, and
`cuda,disk-spill`; workspace `cargo fmt --check` clean.
…tinuation)

Root cause: the merge combined the PR's shared-transcript multi_prove
(prover) with main's per-table-forking multi_verify (verifier). Main evolved
BOTH sides of the non-batched path to fork an independent transcript per table
(idx separator + aux root + table_contribution, per-table z); the PR kept the
older shared-transcript multi_prove and added the batched path as the new
default. The auto-merge took the PR's prover but main's verifier — no textual
conflict flagged it — so their Fiat-Shamir transcripts diverge and grinding
fails ("Grinding factor not satisfied" -> verify_rounds_2_to_4). Only
continuation uses the non-batched pair (monolithic uses batched; the stark
multi-table tests were migrated to batched; single-table soundness tests skip
the num_tables>1 fork), so only continuation broke.

Fix: restore main's per-table non-batched multi_prove so the prover matches
main's verifier that continuation was validated against. Ports main's
multi_prove + its prove_rounds_2_to_4 + free fns plan_table_chunks /
estimate_table_vram_bytes; all build on the SAME low-level primitives
(commit_main_trace, Round1/2/3, round_2/3/4_*) which are byte-identical
between main and the merged tree. The batched path (multi_prove_batched +
prove_rounds_1_to_3) is untouched. Restore the IntoParallelRefIterator import
(main's multi_prove uses par_iter) and drop the now-dead RoundsOneToThree
twiddle_caches field.

Validated: 26/26 continuation tests (incl. tamper-rejection soundness
negatives), 192/192 stark lib tests (batched path intact), clean build on
default/disk-spill/cuda, fmt clean.
…mination)

Conflict: crypto/stark/src/fri/mod.rs — union the two new module decls
(this PR's `pub mod mmcs;` + #729's `pub(crate) mod terminal;`).

Semantic: #729 replaced StarkProof.fri_last_value with fri_final_poly_coeffs
(early-termination). Auto-merge reconciled it everywhere except the batched
verifier's synthetic placeholder proof (batched_synthetic_table_proof), where
fri_last_value: zero() -> fri_final_poly_coeffs: Vec::new(). The two FRI
representations now coexist cleanly: non-batched StarkProof uses #729's
early-termination coeffs; BatchedMultiProof keeps its own unified fri_last_value.
Because main's non-batched multi_prove was ported verbatim, #729's changes to it
applied cleanly through this merge.

Validated: stark lib 211/211, continuation 25/25 (25: #789 removed the flaky
privacy byte-scan test), clean build on default/disk-spill/cuda, fmt clean.
@diegokingston

Copy link
Copy Markdown
Collaborator Author

/bench

CI Lint runs `cargo clippy --workspace --all-targets -- -D warnings`; a newer
clippy (1.94) flags `.clone()` on Copy `FieldElement` at batched.rs:316 and
mmcs.rs:398/688. Drop the redundant clones.

Verified: workspace clippy clean (-D warnings), fmt clean.
Factor multi_prove_batched's Round 4 into a reusable batched_round_4() so the
upcoming continuation epoch driver can share it for the VM-table lane. Also
drop round2s (dead after the DEEP codeword loop) and deep_inputs (dead after
combine_by_height) before the FRI-commit + query-opening phases to lower the
batched peak. Behavior-preserving: 211 stark-lib tests pass.
…iver

STEP A: commit_aux_trace (CPU mirror of commit_main_trace) for the
standalone L2G aux lane.
STEP B: multi_prove_batched_epoch weaves two lanes through one transcript:
absorb L2G main root first, prove_rounds_1_to_3 over the VM tables, fork at
the seam to prove the L2G lane via prove_rounds_2_to_4, then batched_round_4
for the VM tables. Returns (BatchedMultiProof, StarkProof).
STEP C: split batched_multi_verify into batched_verify_rounds_1_to_3
(-> VmMidState) + batched_verify_round_4, behavior-preserving
(batched_multi_verify calls both in sequence). 211 stark-lib tests unchanged.
STEP D: batched_verify_epoch mirrors multi_prove_batched_epoch's transcript
order (L2G main root first, VM rounds 1-3, fork for L2G at the seam, then VM
round 4).
…-root fix

STEP E + wiring: EpochProof now carries vm_proof (BatchedMultiProof) + l2g_proof
(StarkProof); prove_epoch/verify_epoch use multi_prove_batched_epoch /
batched_verify_epoch with the batched COMMIT-bus balance. verify_round_4 folds
L2G's table_contribution into the VM target.

Fix: the expected COMMIT-bus balance now absorbs the standalone L2G main root
FIRST (new Option<&Commitment> param on compute_expected_commit_bus_balance_batched
/ replay_transcript_phase_a_batched), mirroring multi_prove_batched_epoch's
transcript order. Without it the sampled LogUp (z,alpha) diverged and epochs that
commit non-empty output failed to verify (empty output masked it).
- Add AuxCommitTuple<E> alias for commit_aux_trace return (type_complexity)
- Gate now-test-only replay_transcript_phase_a / compute_expected_commit_bus_balance
  under #[cfg(test)] (dead_code in lib target)
- fmt reflow of batched_verify_epoch call site
@diegokingston

Copy link
Copy Markdown
Collaborator Author

/bench

diegokingston and others added 11 commits July 8, 2026 17:56
prove_rounds_1_to_3's air_trace_pairs is &mut Vec, so &air_trace_pairs
in the debug-checks-gated run_debug_checks call was a needless &&mut Vec.
Pass it directly (coerces to &[AirTracePair]). Only the CI clippy pass
with --features lambda-vm-prover/debug-checks compiles this line, so the
default-feature lint was green. multi_prove's owned-Vec call site (3199)
keeps its required &.
…epoch

# Conflicts:
#	crypto/stark/src/prover.rs
…epoch

# Conflicts:
#	crypto/stark/src/verifier.rs
#	prover/src/lib.rs
The two-lane epoch driver builds the L2G aux trace and then commits it
with commit_aux_trace, which reads the aux columns from the HOST trace.
Under cuda the GPU-resident LogUp aux build (set_aux_resident) keeps the
columns device-only and leaves the host trace empty, so the L2G lane
committed a zero aux table against the real bus contribution and every
continuation proof failed the composition-polynomial OOD check at
verification. Disable residency for the L2G trace (same escape hatch
disk-spill uses); the VM lane is unaffected — prove_rounds_1_to_3 has a
device-resident aux commit arm.

Found by proving+verifying an ethrex block with continuations on a GPU
box: CPU builds verified, cuda builds failed, LAMBDA_VM_NO_GPU_LOGUP=1
isolated the aux build, and the monolithic (single-lane) path verifying
cleanly pinned it to the L2G lane.
Brings in the 5 commits the branch was behind (#815 OOD-width guard, #828
streamed field bytes, #800 cuda build robustness, #826 fused/hoisted DEEP
reconstruction, #823 LogUp forward accumulation + g·z OOD pruning).

Conflict:
- crypto/stark/src/tests/bus_tests/soundness_tests.rs (imports): kept both
  sides — `use crate::table::Table;` and
  `use crate::test_utils::{multi_prove_batched_ram, multi_prove_ram};`.

Semantic adaptation of the batched round-4 verifier to the post-#826 API
(main deleted `reconstruct_deep_composition_poly_evaluation`; #823 changed
`step_2_verify_claimed_composition_polynomial`'s signature and added the
`trace_ood_next_evaluations` field to `StarkProof`):

- batched_synthetic_table_proof: add the new `trace_ood_next_evaluations`
  field. The batched proof carries the FULL (unpruned) OOD grid in
  `trace_ood_evaluations`, so the split next-row block is unused on the
  batched path — set it to an empty table.
- batched_verify_round_4 step 2: pass the full OOD grid as `ood_full` plus
  `air.step_size()` to the widened `step_2_verify_claimed_composition_polynomial`.
- batched_verify_round_4 DEEP: replace the two per-point
  `reconstruct_deep_composition_poly_evaluation` calls with a single
  `reconstruct_deep_composition_poly_evaluation_pair` (regular + symmetric),
  hoisting the query-invariant OOD/gamma sums out of the query loop via
  `compute_query_invariant_deep_terms` (#826). Precomputed and main base
  columns are now passed as two borrowed slices (no per-query concat).

The batched path stays a faithful analog of the (pre-pruning) per-table path:
the batched prover commits the full OOD grid and full-width trace-term
coefficients, so `next_row_cols` is every column and no g·z pruning is
applied — the reconstruction sums the whole grid, matching the batched
prover's DEEP codeword exactly. The forward-accumulation constraint change is
inherited through the shared AIR (round-3 OOD, boundary_constraints,
compute_transition); the batched transcript still absorbs the full grid on
both prover and verifier sides, so its Fiat-Shamir semantics are unchanged.
Bring the batched (unified-shard) prove/verify path to full optimization
parity with main's non-batched g·z OOD trace-opening pruning (#823/#827),
so recursive-verifier benchmarks compare like-for-like. Fiat-Shamir changes
for batched proofs — both sides change together; the format is unreleased
draft-PR-only.

Format (`BatchedTableData`):
- `trace_ood_evaluations` now carries only the current-row block (step_size
  rows, all columns); new `trace_ood_next_evaluations` carries the pruned
  next-row block (transition-window columns only, empty when the AIR reads
  none). Mirrors `StarkProof`'s post-#823 split.

Prover:
- `prove_rounds_1_to_3`: split the full OOD via `OodLayout::split_full` and
  absorb the two blocks (current then pruned-next) — same order/semantics as
  the non-batched round-3 absorption.
- `batched_table_deep_codeword`: draw only `layout.num_surviving()` trace-term
  powers and scatter them with `build_trace_term_coeffs` (rectangular grid,
  zeros at pruned positions) — identical DEEP codeword to non-batched round 4.
- `batched_round_4`: store the split blocks in `BatchedTableData`.

Verifier:
- `batched_verify_rounds_1_to_3`: absorb the two split blocks; add the I3
  OOD-shape guard (mirrors `ood_blocks_well_formed`) in Phase A, before Round 3
  absorbs the next-row block. The current-block width is checked against
  `context().trace_columns` (the physical OOD width) rather than
  `trace_layout().0 + num_aux`, since the batched lane includes step-packed
  AIRs (BitFlags) whose `trace_layout().0` is a logical, not physical, count.
- `batched_verify_round_4`: build one `OodLayout` per table (shared across
  the coeff replay, reconstruction, step 2, the #826 query-invariant hoist,
  and the per-query DEEP); draw pruned trace-term coeffs; reconstruct the full
  OOD grid once via `reconstruct_full`; drive the DEEP pair with the AIR-derived
  `next_row_cols` (real g·z pruning, replacing the previous all-columns hoist).
- `batched_synthetic_table_proof`: carry both split blocks.

Both epoch lanes are consistent: the VM lane prunes (this change); the L2G
lane already prunes via `prove_rounds_2_to_4` / `verify_rounds_2_to_4`.
`batched_verify_epoch` and `EpochProof` make no full-grid assumptions.

Tests: three batched soundness negatives for the split format — a current-row
trace-OOD tamper (transcript/step-2 desync) and the two I3 shape guards
(malformed current-row and next-row blocks). Full `stark` suite 225 passing.
…ched verifier)

Parity-sweep follow-up to the batched g·z OOD pruning. `batched_verify_round_4`
built `boundary_coeffs_all`, `transition_coeffs_all`, the pruned
`trace_term_coeffs` grid and `gammas_all`, then index-CLONED each into every
table's `Challenges` — but those vectors are never read again afterward. Move
them via `izip!` instead; only `z` (one field element) and the shared
`rap_challenges` are still cloned per table. Removes a per-table clone of the
DEEP trace-term coefficient grid, which matters most on the guest-side
recursion verifier. Behavior-preserving (identical values); stark suite 225
passing.
@diegokingston

Copy link
Copy Markdown
Collaborator Author

/bench-verify

@github-actions

Copy link
Copy Markdown

Benchmark started on the bench server. The verifier bench takes ~5 min; the recursion-guest cycle comparison then adds guest builds — a few minutes when cached, up to ~1h on a cold run. The bench server is occupied until it finishes.

@github-actions

Copy link
Copy Markdown

Verifier benchmark — 0880cff6b8 vs main (20 pairs)

❌ Run failed. Last log lines:

   Compiling ark-bn254 v0.5.0
   Compiling ethrex-crypto v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-trie v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-common v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling axum-extra v0.10.3
   Compiling ethrex-levm v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-l2-common v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-storage v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-metrics v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-storage-rollup v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-vm v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-blockchain v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-p2p v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-rpc v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-l2-rpc v13.0.0 (https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6)
   Compiling ethrex-fixtures v0.1.0 (/home/app/actions-runner/_work/lambda_vm/lambda_vm/tooling/ethrex-fixtures)
    Finished `release` profile [optimized] target(s) in 34.65s
wrote executor/tests/ethrex_bench_20.bin (32766 bytes): block #1 with 20/20 transfer(s) [N senders -> N recipients]
==> Cached binaries are for different refs/features; rebuilding.
==> Building both cli binaries in isolated worktree /tmp/verify_wt
Preparing worktree (detached HEAD 2baad177)
==> Building cli @ 2baad17712 -> cli_B  (features: jemalloc-stats)
==> Building cli @ 0880cff6b8 -> cli_A  (features: jemalloc-stats)
==> Proving with cli_B (2baad17712)
==> Proving with cli_A (0880cff6b8)
scripts/bench_verify.sh: line 110: 3680459 Killed                  "$1" prove "$ELF" --private-input "$INPUT" -o "$2" --time > "$WORK/prove_$(basename "$2").log" 2>&1
ERROR: prove failed for /tmp/verify_run/cli_A. Tail of log:
Reading ELF file...
Reading private input file...
Generating proof (blowup=2, queries=219)...

Recursion guest cycles (main vs PR)

=== Recursion-guest cycle comparison — single query (blowup=2, 1 query) — deterministic to ~±100k cycles ===
REF_B (baseline) origin/main 2baad17 guest=recursion-min.elf
REF_A (PR) 0880cff 0880cff guest=recursion-min.elf

Metric REF_B (baseline) REF_A (PR) Δ (A-B)
Guest cycles 41.7M 43.2M +1.5M (+3.61%)
Keccak calls 3025 2452 -573

note: cycles reproduce to ~±100k (build codegen + proof nondeterminism); treat sub-100k deltas as noise, not signal.

raw (exact integer counts)
ref_b_sha=2baad177129ebb269022362068d04cefe7ed00c3 ref_b_elf=recursion-min.elf ref_b_cycles=41742640 ref_b_keccak=3025 ref_b_execute_wall_s=1
ref_a_sha=0880cff6b8999fa6830eee6493097f5dbe0a46d5 ref_a_elf=recursion-min.elf ref_a_cycles=43247598 ref_a_keccak=2452 ref_a_execute_wall_s=2
delta_cycles=1504958 delta_keccak=-573

Brings in the two main commits the batched-FRI branch was missing: #844
(supply DECODE / global-memory-genesis roots via private input for continuation
verify) and #845 (verify continuation proofs in place via rkyv). The branch
already carried the rest of main and the batched-lane verifier adaptation.

Conflict resolutions:
- crypto/stark/src/verifier.rs: union the proof-type imports (batched +
  archived view); the batched verifier body is unchanged.
- prover/src/lib.rs: the batched materialize-and-verify path wins wholesale;
  main's #845 zero-copy view verify (verify_proof_parts / MultiProofView) is
  superseded by the batched proof format.
- prover/src/continuation.rs: adopt main's #844/#845 continuation entrypoints
  (continuation_precomputed_commitments, verify_continuation_archived bridge)
  on top of the batched two-lane epoch/global verify.
- prover/src/tests/prove_elfs_tests.rs, prover/src/tests/local_to_global_bus_tests.rs:
  keep the batched verification API (batched_multi_verify /
  verify_l2g_commitment_binding); adopt main's dynamic max_private_input_pages bound.
…tched FRI

Wire the guest-supplied DECODE + page-genesis roots into the batched epoch/global
verify so the continuation recursion guest skips the in-VM DECODE FFT + genesis
Merkle recompute, mirroring main's #844 binding semantics on #768's structure.

- build_epoch_airs / verify_epoch: thread decode_commitment: Option<Commitment>
  into VmAirs::new (supplied DECODE root, shared by every epoch).
- verify_global: add page_genesis_commitments; when Some, classify pages via
  global_memory_configs_classify_only (empty init_values), require every data
  page's genesis root in the supplied set (reject if missing), and resolve each
  gm_air's preprocessed root from the supplied map / hoisted zero-init root.
- global_memory_configs_classify_only uses the light per-page elf_page_has_data
  segment-overlap test (ported verbatim from main's #844), NOT an in-VM paged-image
  build — materializing the image just to split data/zero-init pages would burn
  ~1.21B guest cycles, exactly the work the supplied-roots path exists to skip.
- global_memory_air: take preprocessed: Option<Commitment>
  (unwrap_or_else(recompute)) — None keeps the trustless / prover path.
- verify_continuation split into verify_continuation (trustless None/None),
  verify_continuation_with_roots (supplied roots), and verify_continuation_inner
  (shared, returns entry_point). verify_continuation_archived now consumes the
  supplied roots via the inner path. It still materializes the bundle: the batched
  proof format has no zero-copy ContinuationProofView yet, so this is a deserialize
  cost only and adds no transcript/Merkle hashing.

Supplied roots are used verbatim; genesis binding is deferred to the attestation
fold + consumer recompute, exactly like the monolithic guest's page_commitments.

Adds test_verify_continuation_supplied_roots_matches_trustless: the supplied-roots
verify accepts, equals the trustless output, and a tampered supplied DECODE root
rejects (proving the supplied root is actually consumed).

Also correct recursion.rs verify_continuation_and_attest docs, which described the
continuation path as zero-copy — the batched bridge materializes the bundle.
…etained LDE

The batched path peaked at 1.7-2.0x the per-table prover's RAM because the
three MixedMmcs commitment trees (main, aux, composition) each OWNED a full
copy of every table's LDE, kept for the whole proof solely to serve query
openings after FRI — while the originals stayed resident in round1s/round2s
for DEEP. Committing also built a transient bit-reversed repacking of every
table's LDE before cloning it again into the tree.

Make MixedMmcs store only the digest layers plus each matrix's
(log_height, width). Introduce a LeafSource trait; commit and open_batch read
each row on demand through it. The prover serves those rows from the buffers
it already retains for DEEP via BorrowedMatrix: RowMajorNatural over
round1s[i].lde_trace (main-split and aux columns) and ColMajorNatural over
round2s[i].lde_composition_poly_evaluations. round2s is now held live through
the round-4 opening loop and released right after (it replaces the former
owned composition copy, which lived even longer).

Protocol-neutral: identical roots and identical opened rows — only the leaf
byte source changes. The MMCS bit-reversed row r maps to natural LDE row
reverse_index(r, 2^log_height), matching the existing precomputed-column
opening path. A new equivalence test commits the same matrices through the
owned reference source and the borrowed natural-order sources and asserts
identical roots and openings (row-major, column-major, and the main-split
col_start>0 case); the existing commit_bit_reversed byte-parity tests are
retained through an owning reference source.
… path

In the batched (unified-shard) path every table's main columns are opened
from the ONE shared main MMCS, and the per-table main root is not absorbed
(the shared MMCS root replaces it). The per-table main Merkle tree built in
round 1 is therefore never read on this path — only precomputed_tree,
precomputed_root, and num_precomputed_cols are used — yet it stayed resident
for the whole proof (~one tree of node digests per table).

Give commit_main_trace a `build_main_tree` flag: the reference per-table path
and the standalone L2G lane pass true (they open the per-table tree directly);
the batched path passes false and skips the tree build on the host commit
path. TableCommit::{tree, root} become Option to model "a batched main commit
has no per-table tree"; the precomputed tree/root (the AIR-hardcoded binding,
opened separately) and its mismatch check are still built and enforced in both
cases. The proof format and the verifier are unchanged — the batched verifier
already authenticates via the shared main MMCS root, and the per-table proof
still carries its real main root.

Measured on the 12-table log19 workload this trims the batched prover's peak
live heap by ~370 MiB.
…path

Symmetric to the main-tree skip: on the batched path every aux-carrying table
is opened from the ONE shared aux MMCS, and the per-table aux root is not
absorbed (the shared MMCS root replaces it). The per-table aux Merkle tree
built in round 1 is therefore never read — so skip building it on the host aux
commit path, returning a tree-less TableCommit. The batched aux-commit closure
runs only on the batched path (the reference per-table path and the standalone
L2G aux commit keep their trees), so no flag is needed here.

Measured on the 12-table log19 workload this trims the batched prover's peak
live heap by a further ~390 MiB.
… loops

The batched (unified-shard) prover was ~2.7x slower than the per-table
prover at identical config (worse on higher-core hosts), almost entirely
from parallelism collapse: work the per-table path spreads across tables
(and across leaf hashes) ran serially on the batched path.

- MixedMmcs::commit hashed the base digest layer and every climb level
  serially. It is the batched path's largest single cost — the three
  commits (main, aux, composition) summed to ~8.6s of a ~17s prove in a
  12-table log19 repro. Parallelize base-layer and per-level leaf hashing
  across leaves via crate::par::par_map_collect; it is index-ordered, so
  the root and every layer stay byte-identical to the sequential build.
  ~10x on those commits (~8.6s -> ~0.9s).

- batched_round_4's DEEP-codeword loop and prove_rounds_1_to_3's round-3
  OOD loop iterated tables serially, where the per-table path runs the
  equivalent work inside its parallel rounds-2-4 loop. Compute both in
  parallel across tables, chunked by table_parallelism() to bound the
  co-resident per-table scratch (matching the per-table path's memory
  profile). Round 3's transcript absorption stays sequential in canonical
  order, so the Fiat-Shamir byte sequence is unchanged. Round 3 ~8x,
  round-4 DEEP ~2.6x on the repro.

Prover-side only: roots, proof bytes, and verifier behavior are identical.
Batched-vs-pertable wall-time ratio drops from ~2.7x to ~1.2x on the
12-table log19 repro. Verified by the fri::mmcs root/opening equivalence
tests, the batched soundness suite (valid prove+verify plus tamper
rejections), and the full stark lib suite (226 tests); make lint clean.
…batched path

Symmetric to the main- and aux-tree skips: on the batched (unified-shard)
path every table's composition-poly columns are opened from the ONE shared
composition MMCS, and the per-table composition root is not absorbed (the
shared MMCS root replaces it). The per-table composition Merkle tree built
in round 2 is therefore never read on this path — only its LDE columns
(retained for the shared MMCS and DEEP) are used — yet it stayed resident
for the whole proof.

Give round_2_compute_composition_polynomial a `build_composition_tree` flag:
the reference per-table path passes true (it opens the per-table tree and
absorbs its root), the batched path passes false and skips the tree build on
the host commit path. Round2::{composition_poly_merkle_tree,
composition_poly_root} become Option to model "a batched round 2 has no
per-table composition tree". The proof format and the verifier are unchanged
— the batched verifier authenticates via the shared composition MMCS root,
and the per-table proof still carries its real composition root. The cuda
fused pipeline always builds the tree on-device; the skip applies to the host
commit path (mirrors commit_main_trace's main-tree skip).

Measured on the 12-table log19 workload this trims the batched prover's peak
live heap by ~280 MiB. Combined with the main/aux skips and the round-3/4
parallelization, batched peak drops below per-table (2052 vs 2363 MiB) and
batched wall-time drops to ~0.84x per-table (from ~2.7x). Verified by the
full stark lib suite (226 tests, incl. the batched soundness suite and
prove+verify roundtrips); make lint clean across all four configs.
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.

2 participants