Skip to content

perf(stark): migrate end_exemptions_poly to evaluation form#621

Open
diegokingston wants to merge 1 commit into
mainfrom
perf/end-exemptions-eval-form
Open

perf(stark): migrate end_exemptions_poly to evaluation form#621
diegokingston wants to merge 1 commit into
mainfrom
perf/end-exemptions-eval-form

Conversation

@diegokingston
Copy link
Copy Markdown
Collaborator

Follow-up to #604. Removes transition.rs's last dependency on Polynomial arithmetic — combined with #604 (already merged) no production code in the prover uses Polynomial operators anymore.

Summary

Replaces the coefficient-form end_exemptions_poly: Polynomial<F> in crypto/stark/src/constraints/transition.rs with two eval-form helpers:

  • end_exemptions_roots(trace_primitive_root, trace_length) -> Vec<FieldElement<F>> — the roots rᵢ of ∏(x − rᵢ).
  • end_exemptions_lde_evaluations(domain) -> Vec<FieldElement<F>> — the product evaluated over the precomputed LDE coset directly.

The OOD path in evaluate_zerofier now computes ∏(z − rᵢ) directly instead of Polynomial::evaluate. The Polynomial and evaluate_polynomial_on_lde_domain imports are dropped from transition.rs.

Performance

  • VM path (end_exemptions == 0, the only case in every VM table): unchanged. The else-branch's existing fast-path early-return is preserved bit-for-bit, so the function still returns the short cyclic vector instead of expanding it to the full LDE domain.
  • k > 0 path (example AIRs only — fibonacci variants and read-only-memory): an O(N · end_exemptions) direct product replaces an O(N log N) FFT. Strictly faster for end_exemptions ≤ 2.

Test plan

  • cargo build --workspace — clean.
  • make lint — clean (all three clippy configs).
  • cargo test -p stark — 125/125 passing, including the example AIRs that exercise the eval-form k > 0 path (end_exemptions = 1, 2 in fibonacci variants and read-only-memory).
  • cargo test -p lambda-vm-prover --lib — 273 pass / 77 fail; identical counts on the pre-cleanup(stark/constraints): drop coefficient-form leftovers, dedupe eval #604 baseline (verified by reverting just transition.rs). All 77 failures are pre-existing on the base and unrelated to constraints (UnknownSyscall(5) etc.).

Replaces the coefficient-form `end_exemptions_poly: Polynomial<F>` with
two eval-form helpers, removing transition.rs's last dependency on
`Polynomial` arithmetic. Stacks on #604 (which removed boundary.rs's
`Polynomial` dependency). With both landed, no production code in the
prover uses `Polynomial` operators.

- `end_exemptions_roots` returns the roots r_i of prod(x - r_i)
  (<= 2 in the example AIRs, 0 in every VM table).
- `end_exemptions_lde_evaluations(domain)` evaluates the product over
  the precomputed LDE coset directly: an O(N * end_exemptions) loop
  replaces an O(N log N) FFT.
- `evaluate_zerofier`'s OOD path computes prod(z - r_i) directly
  instead of `Polynomial::evaluate`.

Performance: the VM's dominant path (end_exemptions == 0) is unchanged
- the existing else-branch early-return is preserved bit-for-bit, so
that case still returns the short cyclic vector instead of expanding it.
The k > 0 path (example AIRs only) goes from FFT to direct product,
strictly faster.

Correctness verified by `cargo test -p stark` (125/125, includes the
fibonacci and read-only-memory AIRs with end_exemptions = 1 and 2 -
exactly the eval-form k > 0 path). VM prover lib test counts are
identical to the #604 baseline (273 pass, 77 pre-existing failures
unrelated to constraints).
@github-actions
Copy link
Copy Markdown

Codex Code Review

No issues found in the PR diff.

I was not able to run the Rust tests: cargo/rustup tried to write under read-only /home/runner/.rustup / /home/runner/.cargo, and offline mode then failed on the unresolved git dependency.

@claude
Copy link
Copy Markdown

claude Bot commented May 22, 2026

Code Review

Clean refactoring. The eval-form replacement is correct and the logic is preserved faithfully. A few items worth noting:

Medium — Latent correctness bug: offset != 0 with end_exemptions > 0

The FIXME on line 122 is carried over from the original, but now has sharper consequences: the roots vector is the sole source of truth for which rows are exempt, so a wrong root silently produces a wrong proof for any future constraint with both offset != 0 and end_exemptions > 0. No existing AIR exercises this combination today, but it is easy to miss.

The correct roots should be the last k elements of the offset periodic domain — currently decrement = g^(-period) ignores the offset entirely. The correct value for the last root in the offset domain is g^(offset + (trace_length/period - 1)*period) = g^(trace_length - period + offset).

Low — exemptions_period branch does not skip LDE expansion when end_exemptions == 0

The else branch has a fast-path early return for end_exemptions == 0 (preserving the cyclic vector). The exemptions_period branch calls end_exemptions_lde_evaluations unconditionally: when roots are empty the fold produces a vector of N ones and multiplies through pointlessly. Not a regression (old code also evaluated one_poly over the full domain here), but worth a matching guard if this path ever gets a VM caller.

Nit — double-negation in evaluate_zerofier

The expression -(root.clone() - z.clone()) is a workaround for the subfield/superfield subtraction direction. The comment explains it, but if the math library exposes to_extension(), z.clone() - root.to_extension() reads more directly.


Overall the change is correct, the described performance improvement holds, and tests cover the exercised paths.

// FIXME: when offset != 0 the roots may need to be scaled by
// trace_root^(offset * trace_length / period) — carried over unresolved
// from the original coefficient-form end_exemptions_poly.
let decrement = trace_primitive_root.pow(trace_length - self.period());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The FIXME is pre-existing but the impact is now higher: decrement = g^(trace_length - period) = g^(-period), which gives the correct last root only when offset == 0. When offset != 0, the last row in the offset periodic domain is at g^(trace_length - period + offset), so the roots should start from g^(offset * trace_length / period - (trace_length / period - end_exemptions) * trace_length / period) — or more simply, enumerate the last end_exemptions elements of { g^(offset + j*period) | j = 0..trace_length/period-1 }. A wrong root here produces a silently invalid proof, not an error.

coset_offset,
)
.unwrap();
let end_exemption_evaluations = self.end_exemptions_lde_evaluations(domain);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The else branch has a guard (if self.end_exemptions() == 0 { return evaluations; }) that avoids expanding the full LDE domain when there are no exemptions. This exemptions_period branch lacks the same guard: when end_exemptions == 0, end_exemptions_lde_evaluations iterates over the entire LDE coset to produce a vector of ones and then multiplies through pointlessly. Not a regression from the old code, but worth adding a symmetric guard here for consistency and to close the gap if a VM table ever hits this path.

@diegokingston
Copy link
Copy Markdown
Collaborator Author

/bench 5

@github-actions
Copy link
Copy Markdown

Benchmark — fib_iterative_8M (median of 5)

Table parallelism: auto (cores / 3)

Metric main PR Δ
Peak heap 51992 MB 51823 MB -169 MB (-0.3%) ⚪
Prove time 24.990s 25.078s +0.088s (+0.4%) ⚪

✅ No significant change.

✅ Low variance (time: 1.2%, heap: 0.5%)

Commit: ccab5dc · Baseline: cached · Runner: self-hosted bench

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