Skip to content

Commit 9574292

Browse files
committed
docs: add compiled constraint evaluator design spec
1 parent 7b42e51 commit 9574292

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Compiled Constraint Evaluator
2+
3+
## Problem
4+
5+
The constraint evaluation hot loop in `evaluator.rs` dispatches 77 virtual
6+
calls per LDE row via `Vec<Box<dyn TransitionConstraint>>`. For the CPU table
7+
at 2^20 rows with blowup 4, this is 77 × 8M = 616M indirect calls per proof.
8+
Each call goes through:
9+
1. vtable lookup → function pointer load → branch
10+
2. Prover/Verifier match arm
11+
3. Base-field computation
12+
4. `to_extension()` embed (66 of 77 constraints) — unnecessary 3-limb construction
13+
5. Write to `transition_buf[idx]`
14+
6. Later: dot product of 77 E×E multiplications with beta coefficients
15+
16+
Additional waste:
17+
- 66 `to_extension()` embeds where F×E multiplication would suffice
18+
- `AddConstraint` carry_1 recomputes carry_0 (4 duplicated passes)
19+
- `Frame::fill_from_lde` copies 258 elements into an intermediate buffer
20+
- `transition_buf` zeroed on every iteration despite unconditional overwrites
21+
- Dynamic dispatch on `Multiplicity`, `AddOperand`, `BusValue` enums per row
22+
23+
## Goal
24+
25+
Replace the virtual dispatch loop with a **compiled evaluation function per
26+
table** that:
27+
- Reads columns directly from LDE arrays (no Frame intermediate)
28+
- Computes all constraints in a single pass
29+
- Uses F×E multiplication for base-field constraints (skip `to_extension()`)
30+
- Shares carry_0 computation across carry_1 constraints
31+
- Fuses the zerofier multiplication into the accumulation
32+
- Accumulates directly into a single extension-field result
33+
34+
## Design
35+
36+
### New Trait Method: `evaluate_all_transitions`
37+
38+
Add an optional method to the `AIR` trait (with a default fallback to the
39+
existing `compute_transition_into`):
40+
41+
```rust
42+
trait AIR {
43+
/// Compiled constraint evaluation: computes the weighted sum
44+
/// Σ beta_i * constraint_i(row) directly, bypassing virtual dispatch.
45+
/// Returns the pre-zerofier composition value.
46+
///
47+
/// Default: falls back to compute_transition_into + dot product.
48+
fn evaluate_all_transitions_compiled(
49+
&self,
50+
main_curr: &[FieldElement<F>],
51+
main_next: &[FieldElement<F>],
52+
aux_curr: &[FieldElement<E>],
53+
aux_next: &[FieldElement<E>],
54+
betas: &[FieldElement<E>],
55+
rap_challenges: &[FieldElement<E>],
56+
periodic_values: &[FieldElement<F>],
57+
) -> Option<FieldElement<E>> {
58+
None // default: use virtual dispatch
59+
}
60+
}
61+
```
62+
63+
When `Some(value)` is returned, the evaluator uses it directly (skip
64+
`compute_transition_into` + dot product). When `None`, falls back to existing
65+
path.
66+
67+
### Compiled Evaluator for CPU Table
68+
69+
Implement `evaluate_all_transitions_compiled` for the CPU AIR. The function
70+
body is a hand-written (or macro-generated) Rust function that:
71+
72+
1. Reads specific columns by constant index from the input slices
73+
2. Evaluates all 66 native constraints + 11 LogUp constraints
74+
3. For base-field constraints: `acc += beta[i] * constraint_value` using F×E mul
75+
4. For ext-field constraints (LogUp): `acc += beta[i] * constraint_value` using E×E mul
76+
5. Returns the accumulated sum
77+
78+
### Direct LDE Access (Skip Frame)
79+
80+
Modify `evaluate_transitions` in `evaluator.rs` to pass raw column slices
81+
to `evaluate_all_transitions_compiled` instead of building a `Frame`. The
82+
offsets for current/next row in column-major LDE:
83+
84+
```rust
85+
let curr_offset = i;
86+
let next_offset = (i + step_size * blowup_factor) % lde_size;
87+
// Read: columns[col][curr_offset], columns[col][next_offset]
88+
```
89+
90+
This eliminates the 258-element `fill_from_lde` copy per row.
91+
92+
### Implementation Approach
93+
94+
Rather than hand-writing the compiled function (error-prone for 77
95+
constraints), use a **macro or code-generation** approach:
96+
97+
Option A: Implement `evaluate_all_transitions_compiled` directly in
98+
`prover/src/constraints/cpu.rs` by calling each constraint's `compute()`
99+
method inline without virtual dispatch, accumulating with F×E multiplication.
100+
101+
Option B: Generate the function via a proc-macro that reads the constraint
102+
definitions.
103+
104+
**Recommendation: Option A** — direct implementation in cpu.rs. The
105+
constraints are fixed at compile time, and explicit code is easier to audit
106+
and optimize.
107+
108+
## Files Changed
109+
110+
| File | Change |
111+
|------|--------|
112+
| `crypto/stark/src/traits.rs` | Add `evaluate_all_transitions_compiled` to AIR trait |
113+
| `crypto/stark/src/constraints/evaluator.rs` | Use compiled path when available |
114+
| `prover/src/constraints/cpu.rs` | Implement compiled evaluator for CPU table |
115+
116+
## Expected Impact
117+
118+
- Eliminate 77 vtable dispatches per LDE row (~616M indirect calls)
119+
- Eliminate 66 `to_extension()` embeds per LDE row (replace with F×E mul)
120+
- Eliminate 258-element Frame copy per LDE row
121+
- Share carry_0 computation across carry_1 constraints (save ~4 passes)
122+
- Estimated: ~10-15% total proving time reduction (constraint eval is ~19%)

0 commit comments

Comments
 (0)