Feat/fri early termination#729
Conversation
|
/bench |
Benchmark — ethrex 20 transfers (median of 3)Table parallelism: auto (cores / 3)
Commit: d07779f · Baseline: cached · Runner: self-hosted bench |
|
/bench |
FRI stops folding when the polynomial reaches degree < 2^k (k = ProofOptions.fri_final_poly_log_degree, default 7) instead of folding to a single constant. The prover sends the 2^k final-polynomial coefficients (fri_final_poly_coeffs); the verifier reconstructs the terminal codeword via a coset FFT (base-field twiddles) and checks each query against it, with a structural degree-bound check and a clamp for tiny traces. k is bound into the Fiat-Shamir statement (DOMAIN_TAG _V3). Impact (blowup 2, 219 queries): ~225 KB smaller proof, constant across trace size (~16% at 2^20, 25% at 2^16); verifier does ~6,132 fewer Keccak compressions and ~1,840 fewer Fp3 muls per proof. - Prover interpolates the terminal poly on the minimal 2^k sub-coset (no oversized iFFT, no zero-trim). - GPU FRI commit (cuda) supports early termination, mirroring the CPU path; validated on a CUDA server (proof verifies, gpu_fri_calls fires). - Soundness: tampered / over-length / under-length coeffs and cross-k all reject; terminal codeword<->coeffs roundtrip; single-fold + clamp cases.
c1627b1 to
be46afb
Compare
Resolve conflicts where main's continuations feature meets this branch's FRI early-termination statement binding. Both sides changed absorb_statement's signature, so the merged function takes both kind: StatementKind (continuations) and fri_final_poly_log_degree (this branch); all callers thread both. - continuation.rs: epoch_transcript now binds opts.fri_final_poly_log_degree per epoch, matching the value the epoch's FRI actually folds to (read from air.options() by both prover and verifier). - statement_tests.rs: thread fri_final_poly_log_degree through the test helpers and main's new continuation-epoch tests. - cuda_path_integration.rs: keep both newly added GPU tests (gpu_fri_commit_produces_verifiable_proof and gpu_proof_verifies_row_pair_commitment). - statement.rs: allow clippy::too_many_arguments on absorb_statement, now at 8 args after both features each added one (CI runs clippy -D warnings).
|
/bench |
…ofOptions The main merge brought in MIN_PROOF_OPTIONS (recursion_smoke_test.rs), which constructs ProofOptions without the fri_final_poly_log_degree field this branch adds, breaking compilation of the prover lib tests (Lint, Build prover tests, Disk-spill). Set it to 7 (the default used by every other test ProofOptions).
|
/bench-verify |
|
⏳ Verifier benchmark started on the bench server (~4 min). The bench server is occupied until it finishes. |
Verifier benchmark —
|
| Metric | main | PR | Δ |
|---|---|---|---|
| Verify time (per-side) | 4.300s | 3.912s | +9.03% 🟢 |
| Proof size | 256.86 MiB | 228.31 MiB | +11.12% 🟢 |
Per-side (PR can't deserialize the baseline's proof — proof-format change): A/B/B/A cancels machine drift but not proof-specific variance — read the Verify-time Δ as approximate.
pairs: 20 mean A (PR): 3.912s mean B (main): 4.300s
[parametric] paired-t mean +9.03% sd 0.45% se 0.10%
95% CI: [+8.82%, +9.24%] (t df=19 = 2.093)
[robust] median +9.12% Wilcoxon W+=210 W-=0 p(exact)=1.9e-06 (z=+3.90)
run-to-run jitter: A CV 0.61% B CV 0.50% (lower = steadier)
within-session drift: -0.11% over the run, 1st->2nd half -0.20%
🟢 REAL IMPROVEMENT — PR verifies ~9.03% 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.
…poly_log_degree can't divide-by-zero the prover
|
/bench-verify |
|
⏳ Verifier benchmark started on the bench server (~4 min). The bench server is occupied until it finishes. |
MauroToscano
left a comment
There was a problem hiding this comment.
There are multiple issues wit this
…785) * fix(stark): fall back to CPU when GPU FRI terminal_len == 1 The GPU final fold reuses `fold_and_commit_layer`, whose `assert!(n_out >= 2)` fires when the terminal codeword has length 1 (blowup_log + k == 0). That config only arises from a raw `ProofOptions` literal with `blowup_factor: 1` and `fri_final_poly_log_degree: 0` (every validated constructor rejects blowup 1), but when it does the assert aborts the prover mid-transcript instead of `try_fri_commit_gpu` returning None and letting the CPU fallback (which handles terminal_len == 1) produce the proof — violating the function's documented return-None-on-any-failure contract. Extend the early-return guard to also bail when terminal_len < 2, so the final fold below is always n_out >= 2. * refactor(math-cuda): remove dead fold_final `fold_final` supported the old fold-to-a-constant terminal step (n_out == 1). After the FRI early-termination switch, the GPU final fold goes through `fold_and_commit_layer` and `fold_final` has no callers anywhere. Remove it, and fix the two stale references that still pointed at it (the fault-injection doc and `fold_and_commit_layer`'s assert message). * perf(stark): gather FRI terminal sub-coset via reverse_index `coeffs_from_terminal_codeword` cloned the whole terminal codeword and ran a full O(n) bit-reverse permute only to keep every blowup-th element. Since the codeword is already in bit-reversed order, gather the size-2^k sub-coset directly with reverse_index — no clone, no full permute, and only 2^k of the blowup*2^k evaluations are read. Behaviour is identical (verified by the terminal roundtrip and FRI early-termination tests).
The early-termination fold layout (clamp, total_folds, num_committed, terminal_len, effective_k) was computed independently in three places — `commit_phase_from_evaluations` (CPU prover, from LDE size), `try_fri_commit_gpu` (GPU prover, from n0), and `fri_termination_params` (verifier, from trace bits) — with two different parameterizations whose equivalence lived only in comments. The verifier's own doc comment claimed the arithmetic was centralized "to prevent silent drift", but it was not: an edit to the clamp in one copy would break all proofs, or (worse) only GPU-produced ones, whose parity is not exercised in CI. Introduce `FriFoldLayout::new(lde_log, blowup_log, k)` in fri/terminal.rs and have all three callers derive the layout from it. The single formulation (`terminal_log = min(blowup_log + k, lde_log)`) is also overflow-safe by construction, removing the hand-rolled shift-overflow guards. As part of the same cleanup, the CPU prover now derives the terminal coset offset once as `coset_offset^(2^total_folds)` (matching the GPU and verifier) instead of tracking it by incremental squaring through the fold loop. Wire-identical: full stark prove/verify suite, FRI early-termination soundness tests, and multi-table roundtrip all pass; GPU path compiles under --features cuda.
…d loop `verify_query_and_sym_openings` checked each query against the reconstructed terminal codeword in two places — a dedicated early return for the single-fold (`total_folds == 1`) regime, and the last-iteration `else` arm inside the fold loop for the multi-fold regime. Two hand-synced copies of a soundness-critical comparison (the last-iteration arm being exactly where the padded-decommitment bypass the PR guards against would land) is a drift risk. After the fold loop, `v` and `index` already hold the query's terminal-layer value and position in every regime, so a single check hoisted after the loop covers both — deleting the `is_empty()` early return and the `i < len - 1` last-iteration branch. The per-query decommitment length checks in `step_3_verify_fri` (which pin the fold count) make this behaviour-identical. Verified by the full FRI early-termination soundness suite (empty/padded/ truncated/over-length decommitment rejection across the no-fold, single-fold, and multi-fold regimes) and the prove/verify roundtrips.
…bal statement #729 binds `fri_final_poly_log_degree` into the monolithic statement (STATEMENT_V3) and the continuation-epoch statement (CONTINUATION_EPOCH_V2), but not into `absorb_continuation_global_statement`. The cross-epoch global proof is itself a STARK produced and verified under the same ProofOptions, so its FRI transcript shape depends on `k` just like the others; leaving it unbound makes the canonical-binding guarantee half-applied and contradicts the global statement's own doc comment ("canonically pinned, like the monolithic path's absorb_statement"). Absorb the byte and bump CONTINUATION_GLOBAL_V1 -> V2. A mismatch could only ever reject (the verifier derives every FRI parameter from its own options and structurally checks the proof), so this is defense-in-depth/consistency, not a soundness fix. Adds the `must bind fri_final_poly_log_degree` assertion to the global-statement test; continuation prove/verify roundtrips still pass.
…chedule + terminal check (#787) * refactor(stark): derive the FRI fold layout in one place The early-termination fold layout (clamp, total_folds, num_committed, terminal_len, effective_k) was computed independently in three places — `commit_phase_from_evaluations` (CPU prover, from LDE size), `try_fri_commit_gpu` (GPU prover, from n0), and `fri_termination_params` (verifier, from trace bits) — with two different parameterizations whose equivalence lived only in comments. The verifier's own doc comment claimed the arithmetic was centralized "to prevent silent drift", but it was not: an edit to the clamp in one copy would break all proofs, or (worse) only GPU-produced ones, whose parity is not exercised in CI. Introduce `FriFoldLayout::new(lde_log, blowup_log, k)` in fri/terminal.rs and have all three callers derive the layout from it. The single formulation (`terminal_log = min(blowup_log + k, lde_log)`) is also overflow-safe by construction, removing the hand-rolled shift-overflow guards. As part of the same cleanup, the CPU prover now derives the terminal coset offset once as `coset_offset^(2^total_folds)` (matching the GPU and verifier) instead of tracking it by incremental squaring through the fold loop. Wire-identical: full stark prove/verify suite, FRI early-termination soundness tests, and multi-table roundtrip all pass; GPU path compiles under --features cuda. * refactor(stark): hoist the FRI terminal-codeword check out of the fold loop `verify_query_and_sym_openings` checked each query against the reconstructed terminal codeword in two places — a dedicated early return for the single-fold (`total_folds == 1`) regime, and the last-iteration `else` arm inside the fold loop for the multi-fold regime. Two hand-synced copies of a soundness-critical comparison (the last-iteration arm being exactly where the padded-decommitment bypass the PR guards against would land) is a drift risk. After the fold loop, `v` and `index` already hold the query's terminal-layer value and position in every regime, so a single check hoisted after the loop covers both — deleting the `is_empty()` early return and the `i < len - 1` last-iteration branch. The per-query decommitment length checks in `step_3_verify_fri` (which pin the fold count) make this behaviour-identical. Verified by the full FRI early-termination soundness suite (empty/padded/ truncated/over-length decommitment rejection across the no-fold, single-fold, and multi-fold regimes) and the prove/verify roundtrips.
FRI early-termination review follow-ups (3/3): bind k into the continuation-global statement
…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.
FRI early termination — send final-polynomial coefficients
Motivation
The FRI commit phase folded the deep-composition codeword down to a single
constant (
fri_last_value), so every fold cost a Merkle root plus one symmetricopening per query. This PR terminates folding early instead: once the codeword
encodes a polynomial of degree
< 2^k, the prover sends those2^kcoefficients directly (the standard Plonky3 / ethSTARK approach).
kis a newoption
fri_final_poly_log_degree(default7).Trade-off: we add
2^kfield elements to the proof once, but drop the lastkfold layers —
kMerkle roots andkopenings per query. For productionquery counts this shrinks the proof and cuts commit-phase work, and clamps to a
no-op for traces too small to fold that far.
Description
ProofOptions.fri_final_poly_log_degree: u8(default7).StarkProof.fri_last_value→fri_final_poly_coeffs: Vec<FieldElement<E>>.fri/terminal.rs: pure helpers converting between a terminalcodeword and its polynomial coefficients (
coeffs_from_terminal_codewordprover-side,
terminal_codeword_from_coeffsverifier-side).fri/mod.rs,gpu_lde.rs):commit_phase_from_evaluationsandthe GPU mirror commit
total_folds - 1layers, do one final fold to theterminal codeword, and append its coefficients to the transcript.
verifier.rs): afri_termination_paramshelper centralizes thefold schedule (used by the Fiat-Shamir replay and
step_3_verify_fri); theprover derives the identical schedule inline.
step_3_verify_frireconstructs the terminalcodeword and, before the query loop, checks (1) committed-layer count,
(2)
fri_final_poly_coeffs.len() == 2^expected_k, (3) every per-querydecommitment has exactly
num_committedlayers.verify_query_and_sym_openingscompares each folded query against the reconstructed terminal codeword
(no-fold / single-fold / general cases).
fri_final_poly_log_degreeis absorbed into the statement andthe coefficients replace the final value in the transcript; tags bumped
STATEMENT_V2→V3,CONTINUATION_EPOCH_V1→V2.kcan'tdivide-by-zero the prover (degrades to no early termination).
number_layersparam and unusedDomain.root_orderfield.Soundness
2^expected_kcoefficients, so it is low-degree by construction and theblowup (rate) is preserved — per-query soundness is unchanged from folding to
a constant.
transcript; check (3) pins their length before the fold loop, blocking empty
(vacuous accept) or padded (skip terminal check) decommitments.
Bench results