|
| 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