perf(stark): fuse and hoist deep-composition reconstruction for both FRI points#826
Conversation
…FRI points reconstruct_deep_composition_poly_evaluation walked the OOD table and trace-term coefficients, and inverted denominators, independently for the regular and symmetric evaluation points, then recomputed the point-invariant OOD/gamma sums from scratch on every one of the ~80 FRI queries per proof. Rewriting coeff*(base-ood)*denom as denom*(coeff*base - coeff*ood) isolates the point-independent coeff*ood term (identical between the two points) from coeff*base, so both points can share the OOD walk and a single batch-inverse. The point-invariant ood_row_sum/z_pow/h_sum_zpow sums are now computed once per proof in compute_query_invariant_deep_terms instead of once per query. Multi-query recursion profile: 1,325,335,927 -> 916,824,543 cycles (-30.8%); step 3 (FRI) 635,418,644 -> 233,869,693 cycles (-63.2%).
|
/bench_verifier |
|
/ai-review |
AI ReviewPR #826 · 1 changed files FindingsNo non-rejected structured findings were reported. Reviewer Lanes
Native Codex and Claude reviews run separately and post their own comments. They are not included in this structured provenance report. Raw lane outputs, candidates, final issues, and model metrics are uploaded as workflow artifacts. |
Codex Code ReviewNo issues found in the PR diff. The paired reconstruction preserves the prior algebra and retains malformed-proof rejection checks. |
Benchmark — ethrex 20 transfers (median of 3)Table parallelism: auto (cores / 3)
Commit: 2af8416 · Baseline: cached · Runner: self-hosted bench |
Review: deep-composition fuse + hoistI verified the algebraic rewrite and the guard changes against the diff and the surrounding Correctness — the rewrite is exactThe trace term is regrouped from Soundness — guards are equal-or-stricter
One note (not a defect, not prover-reachable)The pair function now iterates |
|
/bench-verify |
|
⏳ 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. |
Verifier benchmark —
|
| Metric | main | PR | Δ |
|---|---|---|---|
| Verify time | 3.873s | 3.720s | -3.95% 🟢 |
| Proof size | 204.46 MiB | 204.46 MiB | +0.00% ⚪ |
pairs: 20 mean A (PR): 3.720s mean B (main): 3.873s
[parametric] paired-t mean -3.95% sd 0.53% se 0.12%
95% CI: [-4.20%, -3.70%] (t df=19 = 2.093)
[robust] median -3.94% Wilcoxon W+=0 W-=210 p(exact)=1.9e-06 (z=-3.90)
run-to-run jitter: A CV 0.39% B CV 0.65% (lower = steadier)
within-session drift: -0.35% over the run, 1st->2nd half -0.13%
🟢 REAL IMPROVEMENT — PR verifies ~3.95% faster (paired-t and Wilcoxon agree).
Drift-free interleaved A/B/B/A measurement. - = PR faster. Trust the verdict when paired-t and Wilcoxon agree.
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 a8a3d87 guest=recursion-min.elf
REF_A (PR) 2af8416 2af8416 guest=recursion-min.elf
| Metric | REF_B (baseline) | REF_A (PR) | Δ (A-B) |
|---|---|---|---|
| Guest cycles | 72.2M | 68.4M | -3.9M (-5.34%) |
| Keccak calls | 3885 | 3885 | 0 |
note: cycles reproduce to ~±100k (build codegen + proof nondeterminism); treat sub-100k deltas as noise, not signal.
raw (exact integer counts)
ref_b_sha=a8a3d870781b15df7a23e05206796a779e746af5 ref_b_elf=recursion-min.elf ref_b_cycles=72243415 ref_b_keccak=3885 ref_b_execute_wall_s=1
ref_a_sha=2af841676533f7272115610ee8f261b1c91de5eb ref_a_elf=recursion-min.elf ref_a_cycles=68382206 ref_a_keccak=3885 ref_a_execute_wall_s=2
delta_cycles=-3861209 delta_keccak=0
|
I confirmed these numbers with the real Ethrex program proof: −20.32% (1.255×) │ |
|
/ai-review |
Codex Code ReviewNo actionable issues found in the PR diff. The refactoring preserves verifier semantics and retains malformed-proof rejection guards. |
Review: fuse + hoist deep-composition reconstructionReviewed the single changed file ( Math equivalence (verified):
Safety / guards:
Minor (non-blocking): the dimension guard still checks only the product Nice, well-commented change. |
…h verify PR #823 added g·z OOD trace-opening pruning. The layout metadata and the full-grid reconstruction were then derived redundantly at several sites that had to be kept in lockstep by hand: - `num_eval_points`/`next_row_cols` + `num_surviving_trace_openings` + `build_pruned_trace_term_coeffs` recomputed in the verifier round-4 transcript replay and prover round-4. - `reconstruct_ood_full` built TWICE per verify — once in `step_2` (after the `ood_blocks_well_formed` shape guard) and again, unguarded, in `step_3`, which silently relied on `step_2`/Phase A having validated the shapes first. - `split_ood_blocks` args recomputed in prover round-3. Introduce `ood::OodLayout`, a small struct bundling the AIR-derived layout (`num_total_cols`, `num_eval_points`, `step_size`, `next_row_cols`) with methods that forward to the existing free functions (`num_surviving`, `flags`, `build_trace_term_coeffs`, `split_full`, `reconstruct_full`, plus `expected_next_{width,height}` and a `next_row_cols`/`step_size` accessor). The free functions and their pub API are unchanged; the struct only wraps them and adds no new arithmetic. It stays decoupled from the AIR trait — a one-line `ood_layout` helper per crate reads the four raw values and hands them to `OodLayout::new`. `verify_rounds_2_to_4` now builds the layout, runs the `ood_blocks_well_formed` guard, reconstructs the full grid ONCE, and passes borrows into `step_2` and `step_3` (which now take `ood_full`/`step_size`, and `ood_full`/`next_row_cols`/ `step_size` respectively — extending the precedent #826 set when it threaded these through the fused `reconstruct_deep_composition_poly_evaluations_for_all_queries` and `compute_query_invariant_deep_terms`). The guard runs before both steps (same check, same `return false` semantics, same "Composition Polynomial verification failed" log on failure, same order relative to grinding), removing the hidden step_2-before-step_3 ordering dependency and doing one reconstruction instead of two — a small guest-cycle saving on the recursion verifier. The transcript-replay and prover metadata sites derive from the same struct. `ood_blocks_well_formed` is left as-is: its width check uses `trace_layout().0 + num_auxiliary_rap_columns()` (an overridable metadata source I cannot prove equals `trace_columns`), so folding it fully onto OodLayout is not provably behavior-preserving, and a partial fold would force a layout construction in the Phase A hot loop for no net simplification. Zero behavior change: Fiat-Shamir absorption/sampling order is untouched; the metadata sites keep using `trace_columns`; the DEEP reconstruction keeps reading `next_row_cols` on the reconstructed grid exactly as before. The verifier gains no new panics/asserts/unwraps and every `return false` path is preserved; net work strictly decreases (one reconstruction instead of two, no added allocations). Validated with the full `cargo test --release -p stark` suite (196 passed, incl. a new `OodLayout`-delegates-to-free-functions test) and `make lint` (fmt + all four clippy passes incl. cuda) at exit 0.
…h verify (#837) PR #823 added g·z OOD trace-opening pruning. The layout metadata and the full-grid reconstruction were then derived redundantly at several sites that had to be kept in lockstep by hand: - `num_eval_points`/`next_row_cols` + `num_surviving_trace_openings` + `build_pruned_trace_term_coeffs` recomputed in the verifier round-4 transcript replay and prover round-4. - `reconstruct_ood_full` built TWICE per verify — once in `step_2` (after the `ood_blocks_well_formed` shape guard) and again, unguarded, in `step_3`, which silently relied on `step_2`/Phase A having validated the shapes first. - `split_ood_blocks` args recomputed in prover round-3. Introduce `ood::OodLayout`, a small struct bundling the AIR-derived layout (`num_total_cols`, `num_eval_points`, `step_size`, `next_row_cols`) with methods that forward to the existing free functions (`num_surviving`, `flags`, `build_trace_term_coeffs`, `split_full`, `reconstruct_full`, plus `expected_next_{width,height}` and a `next_row_cols`/`step_size` accessor). The free functions and their pub API are unchanged; the struct only wraps them and adds no new arithmetic. It stays decoupled from the AIR trait — a one-line `ood_layout` helper per crate reads the four raw values and hands them to `OodLayout::new`. `verify_rounds_2_to_4` now builds the layout, runs the `ood_blocks_well_formed` guard, reconstructs the full grid ONCE, and passes borrows into `step_2` and `step_3` (which now take `ood_full`/`step_size`, and `ood_full`/`next_row_cols`/ `step_size` respectively — extending the precedent #826 set when it threaded these through the fused `reconstruct_deep_composition_poly_evaluations_for_all_queries` and `compute_query_invariant_deep_terms`). The guard runs before both steps (same check, same `return false` semantics, same "Composition Polynomial verification failed" log on failure, same order relative to grinding), removing the hidden step_2-before-step_3 ordering dependency and doing one reconstruction instead of two — a small guest-cycle saving on the recursion verifier. The transcript-replay and prover metadata sites derive from the same struct. `ood_blocks_well_formed` is left as-is: its width check uses `trace_layout().0 + num_auxiliary_rap_columns()` (an overridable metadata source I cannot prove equals `trace_columns`), so folding it fully onto OodLayout is not provably behavior-preserving, and a partial fold would force a layout construction in the Phase A hot loop for no net simplification. Zero behavior change: Fiat-Shamir absorption/sampling order is untouched; the metadata sites keep using `trace_columns`; the DEEP reconstruction keeps reading `next_row_cols` on the reconstructed grid exactly as before. The verifier gains no new panics/asserts/unwraps and every `return false` path is preserved; net work strictly decreases (one reconstruction instead of two, no added allocations). Validated with the full `cargo test --release -p stark` suite (196 passed, incl. a new `OodLayout`-delegates-to-free-functions test) and `make lint` (fmt + all four clippy passes incl. cuda) at exit 0.
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.
Summary
perf/deep-hoistbranch's deep-composition fuse+hoist (originally against an olderverifier.rs) on top of currentmain'sStarkProofView/precomputed-column-split API.compute_query_invariant_deep_terms.composition_poly_parts_ood_evaluationlength, not just regular-vs-symmetric agreement.Numbers
The headline figure is the base-layer regime: blowup 2 / 219 FRI queries, verifying a real ethrex block (4 transfers, 2 epochs @ 2^21) through the continuation guest. Measured with the PR #825 ladder (
scripts/bench_recursion_scaling.sh,TXS=4 PRESETS=blowup2), base vs head differing only by this PR'sverifier.rs:a8a3d870)2af84167)Keccak calls are identical on both (3,152,604) — the win is field-op restructuring, not hashing. Sanity check: the
beforefigure is +0.94% over the recorded post-#769 ladder value for this cell (13.24B), which is #815's own cost.The other numbers on this PR measure different regimes and should not be read as the Ethrex-relevant win:
make test-profile-recursion-multiPreset::Blowup8over the empty guest — final-wrap params, tiny tables, so deep-composition holds a larger share of the total and the % overstates/bench_verifierguest cyclesPreset::Min= 1 FRI query, empty program — the per-query loop this PR optimizes is barely exercised, so the % understates/bench_verifierverify timeFor reference, the
test-profile-recursion-multifigures:Note that
test-profile-recursion-multicannot express the base-layer regime: its presets are onlyMin(blowup 2, 1 query) andBlowup8. The blowup2/219q preset lands with #825.Test plan
cargo test -p stark --lib— 190/190 passingcargo clippy -p stark --all-targets— cleanmake test-profile-recursion-multibefore/after (numbers above)TXS=4 PRESETS=blowup2 EPOCH_LOG2=21, base vs head (headline numbers above)