Skip to content

perf: base-field constraint evaluation for F×E accumulation#510

Closed
diegokingston wants to merge 2 commits into
mainfrom
feat/base-field-constraint-eval
Closed

perf: base-field constraint evaluation for F×E accumulation#510
diegokingston wants to merge 2 commits into
mainfrom
feat/base-field-constraint-eval

Conversation

@diegokingston

Copy link
Copy Markdown
Collaborator

Summary

Split the prover's constraint evaluation accumulation into base-field (F×E, 3 base muls) and extension-field (E×E, 6 base muls) paths. Table-specific constraints (IS_BIT, ADD, SHIFT, etc.) now write FieldElement<F> to a base-field buffer, while LogUp constraints stay in the extension field.

Based on the approach from PR #418 (which measured -7.4% proving time). Minimal changes to the existing architecture — no new traits, no AirBuilder, no closures.

Changes

  • TransitionConstraint::evaluate_prover() — new method, writes base-field result for main trace constraints
  • AIR::num_base_transition_constraints() — tells evaluator how many constraints are base-field
  • AIR::compute_transition_prover() — split evaluation into base/ext buffers
  • Evaluator hot loop: F×E accumulation for base constraints, E×E for LogUp
  • All 12 table constraint types override evaluate_prover

Test plan

  • All 121 stark tests pass
  • All 266 prover tests pass (60 pre-existing failures unchanged)
  • clippy clean with -D warnings
  • Benchmark: /bench

Add evaluate_prover method to TransitionConstraint trait and split the
evaluator's accumulation loop into F×E (base) and E×E (extension) parts.

Base-field constraints (all table-specific ones) now write to a separate
base-field buffer, enabling cheaper F×E multiplication (3 base muls)
instead of E×E multiplication (6 base muls) during accumulation. LogUp
constraints remain in the extension field via the default implementation.

Changes:
- TransitionConstraint::evaluate_prover with default delegating to evaluate
- AIR::num_base_transition_constraints + compute_transition_prover
- AirWithBuses stores and reports num_base_constraints
- Evaluator uses split accumulation (base_buf + transition_buf)
- All 14 table constraint structs override evaluate_prover
@diegokingston

Copy link
Copy Markdown
Collaborator Author

/bench 3

@github-actions

Copy link
Copy Markdown

Codex Code Review

  1. Medium – Potential soundness regression from unchecked base/extension partition invariant
    • The new prover path assumes constraints are partitioned as 0..num_base (base) and num_base.. (extension), then completely ignores transition_buf[..num_base] during accumulation. If any extension-valued constraint lands in the first partition (or num_base is misreported), that constraint contribution is silently dropped from the composition polynomial.
    • Relevant code:
    • Action: add explicit invariant checks (at least debug_assert!) that num_base <= num_transition_constraints, and that all constraints in 0..num_base are base-writers while extension constraints are strictly in num_base... Without this, a future constraint-ordering change can become a proof-soundness bug.

No other concrete security/bug/performance issues stood out in this diff.

@github-actions

github-actions Bot commented Apr 18, 2026

Copy link
Copy Markdown

Benchmark — fib_iterative_8M (median of 3)

Table parallelism: 32 (auto = cores / 3)

Metric main PR Δ
Peak heap 68340 MB 68982 MB +642 MB (+0.9%) ⚪
Prove time 33.922s 33.580s -0.342s (-1.0%) ⚪

✅ No significant change.

⚠️ Baseline heap spread: 5.8% (66969 MB / 68340 MB / 70933 MB) — comparison may be less reliable

Commit: 85845a3 · Baseline: cached · Runner: self-hosted bench

for e in base_evaluations.iter_mut() {
*e = FieldElement::zero();
}
for e in ext_evaluations[num_base..].iter_mut() {

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.

[High] Silent correctness gap: ext_evaluations[0..num_base] is never zeroed

The docstring says ext_evaluations has length num_transition_constraints(), so slots 0..num_base exist in the buffer but are skipped by the zeroing loop here. The accumulator in evaluator.rs only reads transition_buf[num_base..], so there's no bug today — but this relies on an implicit invariant that base-field constraints only ever write to base_evaluations, never to ext_evaluations[0..num_base].

If a constraint's evaluate_prover override is ever mis-implemented (or the default fallback is called unexpectedly) and writes into ext_evaluations[c_idx] with c_idx < num_base, stale data from a previous iteration would be accumulated silently, producing an incorrect proof with no panic or error.

Consider either zeroing the full ext_evaluations slice here, or adding a debug_assert in the accumulator that transition_buf[0..num_base] are all zero after each compute_transition_prover call.

mut transition_constraints: Vec<Box<dyn TransitionConstraint<F, E>>>,
) -> Self {
let num_interactions = auxiliary_trace_build_data.interactions.len();
let num_base_constraints = transition_constraints.len();

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] Ordering invariant is implicit and unguarded

num_base_constraints is captured here, before LogUp constraints are appended. The correctness of the whole F×E optimization depends on the invariant that all base constraints occupy indices 0..num_base and all LogUp constraints occupy num_base..total. This ordering is never enforced.

If a refactor ever inserts LogUp constraints before the table-specific ones (e.g. a prepend instead of an append), num_base_constraints would silently count LogUp constraints as base-field and the F×E path would produce wrong evaluations.

Consider adding a debug assertion after LogUp constraints are pushed — verifying that every constraint in 0..num_base is not a LogUp type — or at minimum a comment cross-referencing where the append happens.

.zip(transition_coefficients)
.fold(FieldElement::zero(), |acc, (eval, beta)| acc + eval * beta);
z * &sum
let acc_transition = if num_base > 0 {

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] ~90 lines of logic duplicated between parallel and non-parallel paths

The num_base > 0 branch (both is_uniform and non-uniform sub-cases) is copy-pasted nearly verbatim in the #[cfg(not(feature = "parallel"))] path below. This PR doubled the duplication surface compared to the original. Any future correctness fix will need to be applied in two places.

Consider extracting the inner accumulation into a shared free function, e.g.:

fn accumulate_transition<F, E>(
    base_buf: &[FieldElement<F>],
    transition_buf: &[FieldElement<E>],
    num_base: usize,
    transition_coefficients: &[FieldElement<E>],
    zerofier_data: &ZerofierData,
    i: usize,
    is_uniform: bool,
) -> FieldElement<E> { ... }

Both map_init and the sequential loop can then call it with no duplication.

TransitionEvaluationContext::Prover { frame, .. } => {
base_evaluations[self.constraint_idx] = self.compute(frame.get_evaluation_step(0));
}
_ => unreachable!("evaluate_prover called in verifier context"),

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] unreachable! is a runtime panic, not a compile-time guarantee

This pattern is copy-pasted across all 12+ evaluate_prover overrides. It works because evaluate_prover is only called from compute_transition_prover (prover hot loop), but the TransitionConstraint trait is public — any future caller passing a Verifier context would panic at runtime.

A safer alternative is to change the signature so evaluate_prover takes &ProverFrame directly instead of &TransitionEvaluationContext, making the verifier path a compile-time error and eliminating the boilerplate match entirely across all 12 impls.

@claude

claude Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Review: perf: base-field constraint evaluation for F×E accumulation

The optimization is mathematically sound and the -7.4% proving time improvement is well-motivated. The core idea — splitting base-field and extension-field evaluations to use cheaper F×E multiplications — is correct. A few issues worth addressing before merge:

High

  • Unzeroed ext_evaluations[0..num_base] (traits.rs:308): The buffer zeroing skips slots 0..num_base of ext_evaluations. Correctness today relies entirely on an implicit invariant that no constraint writes into those slots. A mis-implemented evaluate_prover override would silently accumulate stale data from the previous iteration with no panic. See inline comment.

Medium

  • Unguarded ordering invariant (lookup.rs:838): num_base_constraints is captured before LogUp constraints are appended. If that ordering ever changes, the base/extension split breaks silently. See inline comment.
  • Severe code duplication (evaluator.rs:116): The accumulation logic is now copy-pasted four times (2 paths × 2 zerofier modes). A shared helper would cut this to one.

Low

  • unreachable! across 12+ impls (cpu.rs:250 and others): Runtime panic rather than compile-time error. Taking &ProverFrame directly in evaluate_prover would eliminate both the panic risk and the boilerplate match.

Replace N separate Box<dyn TransitionConstraint>::evaluate_prover()
vtable calls per LDE point with ONE monolithic function that evaluates
ALL base-field constraints at once. The compiler can now:

- Inline all column reads and arithmetic across constraints
- Share column reads between constraints (CSE)
- Keep field elements in CPU registers across constraints
- Optimize instruction scheduling across constraint boundaries

For CPU table: 66 individual vtable calls → 1 closure call.
Expected: ~10-15% faster constraint evaluation.

The monolithic path is additive — per-constraint fallback still works
for test AIRs and LogUp extension-field constraints.
@diegokingston

Copy link
Copy Markdown
Collaborator Author

/bench 3

@diegokingston
diegokingston deleted the feat/base-field-constraint-eval branch May 20, 2026 12:49
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