Skip to content

Commit a9eb6b9

Browse files
committed
Reused Eq tables
1 parent a0f2b50 commit a9eb6b9

4 files changed

Lines changed: 201 additions & 40 deletions

File tree

src/lagrange_accumulator/accumulator_builder.rs

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use super::{
1616
};
1717
use crate::{
1818
big_num::{DelayedReduction, SmallValue, SmallValueEngine},
19-
polys::{eq::EqPolynomial, eq::compute_suffix_eq_pyramid, multilinear::MultilinearPolynomial},
19+
polys::{eq::build_eq_pyramid, eq::compute_suffix_eq_pyramid, multilinear::MultilinearPolynomial},
2020
};
2121
use ff::PrimeField;
2222
use num_traits::Zero;
@@ -28,12 +28,14 @@ use super::index::compute_idx4;
2828
/// For Spartan's cubic relation (A·B - C), D=2 yields quadratic t_i.
2929
pub const SPARTAN_T_DEGREE: usize = 2;
3030

31-
/// Precomputed eq polynomial tables with balanced split.
31+
/// Precomputed eq polynomial pyramids with balanced split.
3232
struct EqSplitTables<F: PrimeField> {
33-
/// eq(τ[l0..l0+in_vars], ·) evaluations, size 2^in_vars
34-
e_in: Vec<F>,
35-
/// eq(τ[l0+in_vars..], ·) evaluations, size 2^xout_vars
36-
e_xout: Vec<F>,
33+
/// Full pyramid for inner variables: eq(τ[l0..l0+in_vars], ·)
34+
/// Layer k has size 2^k, layer in_vars is the full table (size 2^in_vars)
35+
e_in_pyramid: Vec<Vec<F>>,
36+
/// Full pyramid for outer variables: eq(τ[l0+in_vars..], ·)
37+
/// Layer k has size 2^k, layer xout_vars is the full table (size 2^xout_vars)
38+
e_xout_pyramid: Vec<Vec<F>>,
3739
/// Suffix eq pyramid for prefix variables, Vec per round
3840
e_y: Vec<Vec<F>>,
3941
}
@@ -44,25 +46,37 @@ pub(crate) struct BetaPrefixCache {
4446
num_betas: usize,
4547
}
4648

47-
/// Precompute eq polynomial tables with balanced split for e_in and e_xout.
49+
/// Precompute eq polynomial pyramids with balanced split for e_in and e_xout.
4850
///
4951
/// Returns (tables, in_vars, xout_vars) where:
5052
/// - in_vars = ceil((l - l0) / 2) - variables for inner loop (e_in)
5153
/// - xout_vars = floor((l - l0) / 2) - variables for outer loop (e_xout)
5254
///
5355
/// The balanced split reduces precomputation cost by ~33% compared to the
5456
/// asymmetric l/2 split, and enables odd number of rounds.
57+
///
58+
/// Both e_in and e_xout are returned as full pyramids (not just top layers),
59+
/// enabling reuse by EqSumCheckInstance for the remaining sumcheck rounds.
5560
fn precompute_eq_tables<F: PrimeField>(taus: &[F], l0: usize) -> (EqSplitTables<F>, usize, usize) {
5661
let l = taus.len();
5762
let suffix_vars = l - l0;
5863
let in_vars = suffix_vars.div_ceil(2); // ceiling: e_in larger (inner loop, sequential access)
5964
let xout_vars = suffix_vars - in_vars; // floor: e_xout smaller (outer loop, reused)
6065

61-
let e_in = EqPolynomial::evals_from_points(&taus[l0..l0 + in_vars]); // 2^in_vars entries
62-
let e_xout = EqPolynomial::evals_from_points(&taus[l0 + in_vars..]); // 2^xout_vars entries
66+
// Build full pyramids (not just top layers) for reuse by EqSumCheckInstance
67+
let e_in_pyramid = build_eq_pyramid(&taus[l0..l0 + in_vars]); // in_vars+1 layers
68+
let e_xout_pyramid = build_eq_pyramid(&taus[l0 + in_vars..]); // xout_vars+1 layers
6369
let e_y = compute_suffix_eq_pyramid(&taus[..l0], l0); // Vec per round, total 2^l0 - 1
6470

65-
(EqSplitTables { e_in, e_xout, e_y }, in_vars, xout_vars)
71+
(
72+
EqSplitTables {
73+
e_in_pyramid,
74+
e_xout_pyramid,
75+
e_y,
76+
},
77+
in_vars,
78+
xout_vars,
79+
)
6680
}
6781

6882
/// Build beta → prefix index cache for O(1) scatter access.
@@ -90,6 +104,16 @@ pub(crate) fn build_beta_cache<const D: usize>(l0: usize) -> BetaPrefixCache {
90104
/// - `F`: Field type with small-value and delayed reduction support
91105
/// - `SV`: Witness value type (i32 or i64)
92106
///
107+
/// # Returns
108+
///
109+
/// A tuple of (accumulators, e_in_pyramid, e_xout_pyramid) where:
110+
/// - accumulators: The computed Lagrange accumulators for small-value rounds
111+
/// - e_in_pyramid: Full eq pyramid for inner variables τ[l0..l0+in_vars], has in_vars+1 layers
112+
/// - e_xout_pyramid: Full eq pyramid for outer variables τ[l0+in_vars..ℓ], has xout_vars+1 layers
113+
///
114+
/// The pyramids can be reused by EqSumCheckInstance for the remaining sumcheck rounds,
115+
/// avoiding redundant eq polynomial computation.
116+
///
93117
/// # Parallelism strategy
94118
///
95119
/// - Outer parallel loop over x_out values (using Rayon fold-reduce)
@@ -105,7 +129,7 @@ pub fn build_accumulators_spartan<F, SV>(
105129
bz: &MultilinearPolynomial<SV>,
106130
taus: &[F],
107131
l0: usize,
108-
) -> LagrangeAccumulators<F, 2>
132+
) -> (LagrangeAccumulators<F, 2>, Vec<Vec<F>>, Vec<Vec<F>>)
109133
where
110134
F: SmallValueEngine<SV>,
111135
SV: SmallValue,
@@ -120,10 +144,16 @@ where
120144
let suffix_vars = l - l0;
121145
let prefix_size = 1usize << l0;
122146

123-
// Precompute eq tables with balanced split
124-
let (eq_tables, _in_vars, xout_vars) = precompute_eq_tables(taus, l0);
147+
// Precompute eq pyramids with balanced split
148+
let (eq_tables, in_vars, xout_vars) = precompute_eq_tables(taus, l0);
125149
let num_x_out = 1usize << xout_vars;
126150

151+
// Get top layers (full eq tables) for accumulator computation
152+
let e_in = eq_tables.e_in_pyramid.last().expect("e_in_pyramid non-empty");
153+
let e_xout = eq_tables.e_xout_pyramid.last().expect("e_xout_pyramid non-empty");
154+
debug_assert_eq!(e_in.len(), 1 << in_vars);
155+
debug_assert_eq!(e_xout.len(), 1 << xout_vars);
156+
127157
// Build beta → prefix index cache
128158
let BetaPrefixCache {
129159
cache: beta_prefix_cache,
@@ -145,8 +175,7 @@ where
145175
.e_y
146176
.iter()
147177
.map(|round_ey| {
148-
eq_tables
149-
.e_xout
178+
e_xout
150179
.iter()
151180
.flat_map(|ex| round_ey.iter().map(|ey| *ex * *ey))
152181
.collect()
@@ -156,9 +185,6 @@ where
156185
// Precompute num_y per round for transposed access
157186
let num_y_per_round: Vec<usize> = eq_tables.e_y.iter().map(|ey| ey.len()).collect();
158187

159-
// Borrow e_in directly (no clone needed)
160-
let e_in = &eq_tables.e_in;
161-
162188
// Parallel over x_out with thread-local state (zero per-iteration allocations)
163189
type State<F2, SV2> = SpartanThreadState<F2, SV2, 2>;
164190

@@ -249,9 +275,16 @@ where
249275
.expect("num_x_out > 0 guarantees non-empty fold results");
250276

251277
// Finalize: reduce each bucket from wide 9-limb to field element
252-
merged
278+
let accumulators = merged
253279
.acc
254-
.map(|acc| <F as DelayedReduction<F>>::reduce(acc))
280+
.map(|acc| <F as DelayedReduction<F>>::reduce(acc));
281+
282+
// Return accumulators along with full pyramids for EqSumCheckInstance reuse
283+
(
284+
accumulators,
285+
eq_tables.e_in_pyramid,
286+
eq_tables.e_xout_pyramid,
287+
)
255288
}
256289

257290
#[cfg(test)]
@@ -283,7 +316,7 @@ mod tests {
283316

284317
let taus: Vec<Scalar> = vec![Scalar::from(3u64), Scalar::from(5u64)];
285318

286-
let acc = build_accumulators_spartan(&az, &bz, &taus, l0);
319+
let (acc, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
287320

288321
// Only round 0 exists (v is empty). β ranges over U_d with binary {0,1} and non-binary {∞}.
289322
// Buckets for u = 0 should be zero (binary β), bucket for u = ∞ should be non-zero.
@@ -331,8 +364,8 @@ mod tests {
331364
];
332365

333366
// Build accumulators twice
334-
let acc1 = build_accumulators_spartan(&az, &bz, &taus, l0);
335-
let acc2 = build_accumulators_spartan(&az, &bz, &taus, l0);
367+
let (acc1, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
368+
let (acc2, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
336369

337370
// Compare all buckets
338371
for round in 0..l0 {
@@ -369,8 +402,8 @@ mod tests {
369402
let taus: Vec<Scalar> = (0..l).map(|i| Scalar::from((i * 7 + 3) as u64)).collect();
370403

371404
// Build accumulators twice to verify consistency
372-
let acc1 = build_accumulators_spartan(&az, &bz, &taus, l0);
373-
let acc2 = build_accumulators_spartan(&az, &bz, &taus, l0);
405+
let (acc1, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
406+
let (acc2, _, _) = build_accumulators_spartan(&az, &bz, &taus, l0);
374407

375408
for round in 0..l0 {
376409
let num_v = (D + 1).pow(round as u32);

src/polys/eq.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,57 @@ impl<Scalar: PrimeField> FromIterator<Scalar> for EqPolynomial<Scalar> {
8585
}
8686
}
8787

88+
/// Build a full eq polynomial pyramid from tau values.
89+
///
90+
/// Given taus = [τ₀, τ₁, ..., τ_{n-1}], builds a pyramid where:
91+
/// - Layer 0: [1]
92+
/// - Layer 1: [1-τ_{n-1}, τ_{n-1}]
93+
/// - Layer k: eq([τ_{n-k}, ..., τ_{n-1}], ·), size 2^k
94+
/// - Layer n: eq([τ₀, ..., τ_{n-1}], ·), size 2^n (the full eq table)
95+
///
96+
/// The pyramid has n+1 layers total. This structure matches EqSumCheckInstance's
97+
/// internal representation, enabling pyramid reuse between accumulator building
98+
/// and sumcheck evaluation.
99+
///
100+
/// The taus are processed in reverse order (last tau first) to match the
101+
/// standard multilinear indexing convention where the first tau corresponds
102+
/// to the most significant bit of the index.
103+
///
104+
/// Each layer is stored as [lo_0, lo_1, ..., lo_n, hi_0, hi_1, ..., hi_n]
105+
/// where lo values are multiplied by (1-τ) and hi values by τ.
106+
pub fn build_eq_pyramid<S: PrimeField>(taus: &[S]) -> Vec<Vec<S>> {
107+
use rayon::prelude::*;
108+
109+
let n = taus.len();
110+
let mut pyramid = Vec::with_capacity(n + 1);
111+
112+
// Layer 0: base case [1]
113+
pyramid.push(vec![S::ONE]);
114+
115+
// Build layers 1..n by adding one tau at a time (in reverse order)
116+
// Uses the same parallel pattern as EqSumCheckInstance::new
117+
for i in 0..n {
118+
let tau = taus[n - 1 - i]; // Process taus in reverse
119+
let prev = &pyramid[i];
120+
121+
// Build next layer: [lo_0, ..., lo_n, hi_0, ..., hi_n]
122+
// First, copy prev and extend with hi values (prev * tau)
123+
let mut next = prev.to_vec();
124+
next.par_extend(prev.par_iter().map(|v| *v * tau));
125+
126+
// Then subtract hi from lo: lo = prev - hi = prev * (1 - tau)
127+
let (first, last) = next.split_at_mut(prev.len());
128+
first
129+
.par_iter_mut()
130+
.zip(last)
131+
.for_each(|(a, b)| *a -= *b);
132+
133+
pyramid.push(next);
134+
}
135+
136+
pyramid
137+
}
138+
88139
/// Compute suffix eq tables for small-value sumcheck optimization.
89140
///
90141
/// Given τ = (τ₀, τ₁, ..., τ_{l-1}) and `l0` (the number of small-value rounds),

src/small_sumcheck.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,13 +243,13 @@ where
243243

244244
// ===== Pre-computation Phase =====
245245
// Build accumulators A_i(v, u) for all i ∈ [ℓ₀] using small-value arithmetic.
246+
// Also builds eq pyramids for reuse by EqSumCheckInstance.
246247
// Internally computes eq tables with balanced split and precomputed eq_cache.
247248
// Uses: small × small → intermediate (for Az·Bz products),
248249
// then intermediate × field (for eq weighting via DelayedReduction).
249-
let accumulators = build_accumulators_spartan(poly_A_small, poly_B_small, &taus, l0);
250+
let (accumulators, mut e_in_pyramid, e_xout_pyramid) =
251+
build_accumulators_spartan(poly_A_small, poly_B_small, &taus, l0);
250252

251-
// Create EqSumCheckInstance for suffix variables (used in remaining rounds).
252-
let mut eq_instance = eq_sumcheck::EqSumCheckInstance::<E>::new(&taus[l0..]);
253253
let mut small_value_sumcheck =
254254
SmallValueSumCheck::<E::Scalar, SPARTAN_T_DEGREE>::from_accumulators(accumulators);
255255

@@ -306,11 +306,21 @@ where
306306
);
307307

308308
// ===== Remaining Rounds (ℓ₀ to ℓ-1) =====
309-
// Inject the accumulated eq factor from small-value rounds into eq_instance.
310-
// This incorporates eq(τ_{0..l0}, r_{0..l0}) so the pyramid results are correct.
311-
eq_instance.set_eval_eq_left(small_value_sumcheck.eq_alpha());
309+
// Pop the top layer from e_in_pyramid. The top layer was used by the accumulator;
310+
// EqSumCheckInstance needs the remaining layers (without the first suffix tau τ[l₀]).
311+
// This matches the Nova optimization where τ[l₀] is tracked in eval_eq_left.
312+
e_in_pyramid.pop();
313+
314+
// Create EqSumCheckInstance from precomputed pyramids, avoiding redundant eq computation.
315+
// The accumulated eq factor from small-value rounds is passed directly.
316+
let mut eq_instance = eq_sumcheck::EqSumCheckInstance::<E>::from_pyramids(
317+
e_in_pyramid,
318+
e_xout_pyramid,
319+
&taus[l0..],
320+
small_value_sumcheck.eq_alpha(),
321+
);
312322

313-
// Continue with the remaining rounds using the REUSED eq instance.
323+
// Continue with the remaining rounds using the eq instance built from pyramids.
314324
// The eq_instance was created with taus[l0..], so its internal round counter tracks
315325
// local rounds. We pass global round index for the round_idx parameter since
316326
// eval_one_case_cubic_three_inputs uses it for a round-0 optimization.
@@ -421,7 +431,7 @@ mod tests {
421431
let mut claim = F::ZERO;
422432

423433
// Build accumulators using the simplified API
424-
let accs = build_accumulators_spartan(&az_poly, &bz_poly, &taus, SMALL_VALUE_ROUNDS);
434+
let (accs, _, _) = build_accumulators_spartan(&az_poly, &bz_poly, &taus, SMALL_VALUE_ROUNDS);
425435
let mut small_value = SmallValueSumCheck::from_accumulators(accs);
426436

427437
// Full eq_instance for verification against standard sumcheck

src/sumcheck.rs

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -957,14 +957,81 @@ pub(crate) mod eq_sumcheck {
957957
}
958958
}
959959

960-
/// Sets the accumulated eq evaluation factor from previous rounds.
960+
/// Creates an EqSumCheckInstance from precomputed eq pyramids.
961961
///
962-
/// This allows reusing an EqSumCheckInstance created for suffix variables
963-
/// while incorporating the eq factor from prefix rounds (small-value rounds).
964-
/// Call this before starting the remaining rounds to inject the factor
965-
/// `eq(τ_{0..l0}, r_{0..l0})` accumulated during small-value rounds.
966-
pub fn set_eval_eq_left(&mut self, factor: E::Scalar) {
967-
self.eval_eq_left = factor;
962+
/// This constructor enables reuse of pyramids already computed during accumulator
963+
/// building, avoiding redundant eq polynomial computation.
964+
///
965+
/// # Arguments
966+
///
967+
/// - `e_in_pyramid`: Pyramid for inner variables (ALREADY POPPED by caller).
968+
/// Has `in_vars` layers where layer k has size 2^k.
969+
/// Built from τ[l₀+1 : l₀+in_vars] (first suffix tau τ[l₀] is excluded).
970+
/// - `e_xout_pyramid`: Full pyramid for outer variables.
971+
/// Has `xout_vars+1` layers where layer k has size 2^k.
972+
/// Built from τ[l₀+in_vars : ℓ].
973+
/// - `taus`: The suffix tau values τ[l₀:ℓ] (needed for `bound()` and `eq_tau_0_2_3`).
974+
/// - `eval_eq_left`: Accumulated eq factor from prefix rounds, eq(τ[0:l₀], r[0:l₀]).
975+
///
976+
/// # Factorization
977+
///
978+
/// The eq polynomial factors as:
979+
/// ```text
980+
/// eq(τ[l₀:ℓ], x) = eq(τ[l₀], x₀) × eq(τ[l₀+1:l₀+in_vars], x_in) × eq(τ[l₀+in_vars:ℓ], x_out)
981+
/// └─ tracked in eval_eq_left ─┘ └── e_in_pyramid ──┘ └── e_xout_pyramid ──┘
982+
/// ```
983+
///
984+
/// The first suffix tau τ[l₀] is handled via `eval_eq_left` and `bound()`,
985+
/// matching the Nova optimization where the first variable is tracked separately.
986+
pub fn from_pyramids(
987+
e_in_pyramid: Vec<Vec<E::Scalar>>,
988+
e_xout_pyramid: Vec<Vec<E::Scalar>>,
989+
taus: &[E::Scalar],
990+
eval_eq_left: E::Scalar,
991+
) -> Self {
992+
// in_vars = number of layers in popped pyramid (one less than original)
993+
// After pop: e_in_pyramid has in_vars layers for in_vars-1 taus
994+
// But first_half should equal in_vars (the number of rounds for left side)
995+
let in_vars = e_in_pyramid.len();
996+
let xout_vars = e_xout_pyramid.len().saturating_sub(1);
997+
998+
debug_assert_eq!(
999+
in_vars + xout_vars,
1000+
taus.len(),
1001+
"Pyramid sizes must match taus length: in_vars={}, xout_vars={}, taus.len()={}",
1002+
in_vars,
1003+
xout_vars,
1004+
taus.len()
1005+
);
1006+
1007+
// Compute eq_tau_0_2_3 for all suffix taus (same logic as new())
1008+
// These are precomputed eq evaluations at points 0, 2, 3:
1009+
// eq(τ, 0) = 1 - τ
1010+
// eq(τ, 2) = 3τ - 1
1011+
// eq(τ, 3) = 5τ - 2
1012+
let f2 = E::Scalar::ONE.double();
1013+
let f1 = E::Scalar::ONE;
1014+
let eq_tau_0_2_3 = taus
1015+
.par_iter()
1016+
.map(|tau| {
1017+
let tau2 = tau.double();
1018+
let tau3 = tau2 + tau;
1019+
let tau5 = tau3 + tau2;
1020+
(f1 - tau, tau3 - f1, tau5 - f2)
1021+
})
1022+
.collect::<Vec<_>>();
1023+
1024+
Self {
1025+
init_num_vars: in_vars + xout_vars,
1026+
first_half: in_vars,
1027+
second_half: xout_vars,
1028+
round: 1,
1029+
taus: taus.to_vec(),
1030+
eval_eq_left,
1031+
poly_eq_left: e_in_pyramid,
1032+
poly_eq_right: e_xout_pyramid,
1033+
eq_tau_0_2_3,
1034+
}
9681035
}
9691036

9701037
/// Evaluate poly_A * poly_B - poly_C

0 commit comments

Comments
 (0)