Skip to content

Cache LDE + Scaling improvements#494

Closed
gabrielbosio wants to merge 7 commits into
feat/cache-lde-round1from
feat/cache-lde-scaling
Closed

Cache LDE + Scaling improvements#494
gabrielbosio wants to merge 7 commits into
feat/cache-lde-round1from
feat/cache-lde-scaling

Conversation

@gabrielbosio

@gabrielbosio gabrielbosio commented Apr 13, 2026

Copy link
Copy Markdown
Collaborator

Merge of #492 with the first 5 commits of #483. Just to check bench numbers.

diegokingston and others added 7 commits April 13, 2026 17:15
Tables with effective width < 42 were sized at 2^21, producing single
large chunks. Capping at 2^20 produces 2x more chunks of the standard
size, improving parallel throughput and keeping peak memory per chunk
uniform across all tables.
Tables sharing the same lde_size now reuse a single Arc<LdeTwiddles>
instead of each allocating their own copy. This avoids redundant twiddle
computation and allocation when multiple tables happen to have the same
trace length (e.g., all 2^19-row tables after the cap change).
Under the `parallel` feature, fold each conjugate pair concurrently
using `par_chunks(2).zip(inv_twiddles.par_iter())`, collecting into a
new Vec of half length. The sequential path (no `parallel` feature) is
unchanged. This avoids the index aliasing issue from an in-place
interleaved write.
Replace the per-row `Vec` allocation inside the iterator with a
thread-local buffer via Rayon's `map_init` (parallel path) or a single
buffer allocated outside the loop (sequential path). This avoids one
heap allocation per row during Merkle tree construction.
@gabrielbosio

Copy link
Copy Markdown
Collaborator Author

/bench

@github-actions

Copy link
Copy Markdown

Codex Code Review

  1. Medium – Correctness/DoS risk from over-broad twiddle cache key
    crypto/stark/src/prover.rs:1547, crypto/stark/src/prover.rs:1556, crypto/stark/src/prover.rs:183
    twiddle_by_size is keyed only by lde_size, but LdeTwiddles::new(domain) depends on interpolation_domain_size, blowup_factor, and coset_offset.
    If two AIRs in multi_prove share lde_size but differ in options, they can incorrectly reuse twiddles. This can yield wrong commitments/openings or trigger panics (expect("coset LDE expansion")).
    Action: key the cache by all relevant domain parameters (at least (interpolation_domain_size, blowup_factor, coset_offset)), or enforce/assert identical proof options across all AIRs before caching.

  2. Low – Parallel FRI fold silently weakens invariant checks
    crypto/stark/src/fri/fri_functions.rs:25
    Parallel path uses par_chunks(2).zip(inv_twiddles.par_iter()), which truncates to the shorter side. If lengths diverge, output is silently shortened instead of failing fast (non-parallel path would panic on inv_twiddles[j]).
    Also, pair[1] will panic for odd evals.len() in parallel path.
    Action: add explicit precondition checks (evals.len() % 2 == 0, inv_twiddles.len() == evals.len()/2) before folding to keep behavior deterministic across feature flags.

No other security/correctness/performance issues stood out in this PR diff.

Comment on lines +1556 to +1559
let lde_size = domain.interpolation_domain_size * domain.blowup_factor;
let twiddles = twiddle_by_size
.entry(lde_size)
.or_insert_with(|| Arc::new(LdeTwiddles::new(&domain)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium – Incorrect cache key if AIRs have different blowup_factor or coset_offset

LdeTwiddles::new bakes three domain-specific quantities into the struct:

  • inv twiddles — sized by interpolation_domain_size
  • fwd twiddles — sized by lde_size
  • coset_weights — sized by interpolation_domain_size, scaled by coset_offset

Using only lde_size as the cache key is safe only when all AIRs share the same blowup_factor (so equal lde_size ⟹ equal interpolation_domain_size) and the same coset_offset. The comment acknowledges this but it isn't enforced. If violated, twiddles from one domain are silently reused for another, producing cryptographically wrong (but non-panicking) proofs.

Consider asserting the invariant at the cache hit, e.g.:

// or store (interpolation_domain_size, coset_offset) as key
debug_assert_eq!(
    domain.interpolation_domain_size,
    first_domain_with_this_lde_size.interpolation_domain_size
);

Or use (lde_size, coset_offset_bits) as the key to make the assumption self-enforcing.

Comment on lines +25 to +27
let folded: Vec<FieldElement<E>> = evals
.par_chunks(2)
.zip(inv_twiddles.par_iter())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low – Parallel and sequential branches diverge on contract violation

If inv_twiddles.len() < evals.len() / 2 (broken contract):

  • parallel: zip silently truncates to the shorter iterator → folded is shorter than evals.len() / 2, and *evals = folded quietly installs a wrong-length vector. Downstream code proceeds with corrupted state.
  • sequential: evals[j] with j reaching half - 1 will index out of inv_twiddles → panic.

In practice the contract is maintained by callers, but if it breaks the parallel path gives no diagnostic while the sequential path at least terminates loudly. A debug_assert_eq!(inv_twiddles.len(), evals.len() / 2) at the top of the function would catch this in both builds without runtime cost in release.

@claude

claude Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Review: Cache LDE + Scaling improvements

Overall the changes are well-structured. Two issues worth addressing, plus a note on the table-size reductions.


Medium – Twiddle cache key is under-specified

See inline comment on prover.rs:1556-1559.

LdeTwiddles encodes interpolation_domain_size, lde_size, and coset_offset (via coset_weights). Keying the cache on lde_size alone is correct only when all AIRs share the same blowup_factor and coset_offset. The comment documents the assumption but doesn't enforce it. A violated assumption silently reuses wrong twiddles, producing incorrect proofs without any panic or error.

Suggested fix: assert the invariant at cache-hit time or use (interpolation_domain_size, coset_offset) as the key.


Low – Parallel/sequential diverge on fold_evaluations_in_place contract violation

See inline comment on fri_functions.rs:25-27.

If inv_twiddles.len() < evals.len() / 2, the parallel path silently produces a shorter-than-expected vector while the sequential path panics. Contract is maintained today, but a debug_assert_eq!(inv_twiddles.len(), evals.len() / 2) at the function top would make both paths fail loudly and symmetrically.


Low – max_rows reductions (2^21 to 2^20) should be guarded at runtime

LT, LOAD, BRANCH, MEMW_R all halved. If a program generates more than 2^20 rows for any of these tables, is there a runtime check that returns an error rather than silently truncating or overflowing? Worth confirming that the table-filling code enforces this limit explicitly.


The parallelisation of fold_evaluations_in_place and the commit_columns_bit_reversed row-buffer reuse are both correct and clean.

@github-actions

Copy link
Copy Markdown

Benchmark — fib_iterative_8M (median of 3)

Table parallelism: 32 (auto = cores / 3)

Metric main PR Δ
Peak heap 221209 MB 66330 MB -154879 MB (-70.0%) 🟢
Prove time 46.611s 34.997s -11.614s (-24.9%) 🟢

🎉 Improvement detected — heap or time decreased by more than 5%.

✅ Low variance (time: 2.7%, heap: 4.2%)

Commit: ec49bf6 · Baseline: built from main · Runner: self-hosted bench

@gabrielbosio

Copy link
Copy Markdown
Collaborator Author

Not a great difference

@gabrielbosio
gabrielbosio deleted the feat/cache-lde-scaling branch April 20, 2026 18:15
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