Skip to content

Commit 1410059

Browse files
authored
Merge branch 'main' into continuations-local-to-global
2 parents ee19328 + e3dd2d1 commit 1410059

2 files changed

Lines changed: 129 additions & 25 deletions

File tree

crypto/stark/src/prover.rs

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
use std::marker::PhantomData;
2-
use std::sync::Arc;
2+
use std::sync::{Arc, OnceLock};
33
#[cfg(feature = "instruments")]
44
use std::time::{Duration, Instant};
55

66
use crypto::fiat_shamir::is_transcript::IsStarkTranscript;
77
use math::fft::bit_reversing::{in_place_bit_reverse_permute, reverse_index};
8-
#[cfg(any(test, feature = "test-utils", feature = "debug-checks"))]
98
use math::fft::bowers_fft::LayerTwiddles;
109
use math::fft::errors::FFTError;
1110
use math::fft::two_half_fft::TwoHalfTwiddles;
@@ -292,11 +291,55 @@ pub(crate) struct LdeTwiddles<F: IsFFTField> {
292291
two_half_inv: TwoHalfTwiddles<F>,
293292
two_half_fwd: TwoHalfTwiddles<F>,
294293
coset_weights: Vec<FieldElement<F>>,
294+
/// Composition half-extension cache, initialized only when the degree-2
295+
/// decomposition path actually runs on CPU.
296+
composition: OnceLock<CompositionLdeTwiddles<F>>,
297+
}
298+
299+
pub(crate) struct CompositionLdeTwiddles<F: IsFFTField> {
300+
/// Inverse twiddles for the g²-coset halves of size `lde_size/2`.
301+
inv: LayerTwiddles<F>,
302+
/// Forward twiddles for the full g-coset of size `lde_size`.
303+
fwd: LayerTwiddles<F>,
304+
/// Weights `g⁻ʲ/(lde_size/2)` for the composition half-extension.
305+
weights: Vec<FieldElement<F>>,
306+
}
307+
308+
impl<F: IsFFTField> CompositionLdeTwiddles<F> {
309+
fn new(half_size: usize, offset: &FieldElement<F>) -> Self {
310+
// Composition half-extension weights: g⁻ʲ / half_size. The constraint-
311+
// quotient halves live on the g²-coset of size `half_size`; the unnormalized
312+
// iFFT yields `n·cⱼ·(g²)ʲ` and these weights turn that into `cⱼ·gʲ` for the
313+
// forward FFT onto the g-coset.
314+
let half_size_fe = FieldElement::<F>::from(half_size as u64);
315+
let inv_half_size_offset = (&half_size_fe * offset)
316+
.inv()
317+
.expect("half_size and coset offset are non-zero");
318+
let half_size_inv = offset * &inv_half_size_offset;
319+
let offset_inv = &half_size_fe * &inv_half_size_offset;
320+
let weights = {
321+
let mut w = Vec::with_capacity(half_size);
322+
let mut cur = half_size_inv;
323+
for _ in 0..half_size {
324+
w.push(cur.clone());
325+
cur = &cur * &offset_inv;
326+
}
327+
w
328+
};
329+
330+
Self {
331+
inv: LayerTwiddles::<F>::new_inverse(half_size.trailing_zeros() as u64)
332+
.expect("valid composition inverse twiddles"),
333+
fwd: LayerTwiddles::<F>::new((half_size * 2).trailing_zeros() as u64)
334+
.expect("valid composition forward twiddles"),
335+
weights,
336+
}
337+
}
295338
}
296339

297340
impl<F: IsFFTField> LdeTwiddles<F> {
298341
/// Construct twiddles and coset weights for a domain of the given size and blowup factor.
299-
fn new(domain: &Domain<F>) -> Self {
342+
pub(crate) fn new(domain: &Domain<F>) -> Self {
300343
let domain_size = domain.interpolation_domain_size;
301344
let lde_size = domain_size * domain.blowup_factor;
302345

@@ -326,8 +369,22 @@ impl<F: IsFFTField> LdeTwiddles<F> {
326369
two_half_fwd: TwoHalfTwiddles::<F>::new(lde_size.trailing_zeros() as usize, false)
327370
.expect("valid forward two-half twiddles"),
328371
coset_weights,
372+
composition: OnceLock::new(),
329373
}
330374
}
375+
376+
fn composition(&self, domain: &Domain<F>) -> &CompositionLdeTwiddles<F> {
377+
let lde_size = domain.interpolation_domain_size * domain.blowup_factor;
378+
let half_size = lde_size / 2;
379+
debug_assert_eq!(self.coset_weights.len(), domain.interpolation_domain_size);
380+
self.composition
381+
.get_or_init(|| CompositionLdeTwiddles::new(half_size, &domain.coset_offset))
382+
}
383+
384+
#[cfg(test)]
385+
pub(crate) fn has_composition_cache(&self) -> bool {
386+
self.composition.get().is_some()
387+
}
331388
}
332389

333390
/// Number of tables to process concurrently in `multi_prove`.
@@ -1120,6 +1177,7 @@ pub trait IsStarkProver<
11201177
fn decompose_and_extend_d2(
11211178
constraint_evaluations: &[FieldElement<FieldExtension>],
11221179
domain: &Domain<Field>,
1180+
twiddles: &LdeTwiddles<Field>,
11231181
) -> Vec<Vec<FieldElement<FieldExtension>>>
11241182
where
11251183
FieldElement<Field>: AsBytes + Sync + Send,
@@ -1150,9 +1208,8 @@ pub trait IsStarkProver<
11501208
(&two_inv * &sum, &inv_2x[i] * &diff)
11511209
});
11521210

1153-
// Step 3: Extend each part from N evals on g²-coset to 2N evals on g-coset.
1154-
// The squared coset offset is g² (= coset_offset²).
1155-
let coset_offset_squared = &domain.coset_offset * &domain.coset_offset;
1211+
// Step 3: Extend each part from n evals on the g²-coset to 2n evals on the
1212+
// g-coset (the full LDE domain).
11561213

11571214
// GPU fast path: batch both halves into one ext3 LDE call. Requires
11581215
// `cuda` feature and a qualifying size. Falls through to CPU when not.
@@ -1163,43 +1220,46 @@ pub trait IsStarkProver<
11631220
return vec![lde_h0, lde_h1];
11641221
}
11651222

1223+
let composition_twiddles = twiddles.composition(domain);
11661224
let (lde_h0, lde_h1) = crate::par::join(
1167-
|| Self::extend_half_to_lde(&h0_evals, &coset_offset_squared, domain),
1168-
|| Self::extend_half_to_lde(&h1_evals, &coset_offset_squared, domain),
1225+
|| Self::extend_half_to_lde(&h0_evals, composition_twiddles),
1226+
|| Self::extend_half_to_lde(&h1_evals, composition_twiddles),
11691227
);
11701228
vec![lde_h0, lde_h1]
11711229
}
11721230

1173-
/// Given N evaluations of a degree-<N polynomial on the g²-coset,
1174-
/// extend to 2N evaluations on the g-coset (the full LDE domain).
1175-
/// This is: iFFT(N, offset=g²) → coefficients → FFT(2N, offset=g).
1231+
/// Extend `half_evals` — `n = lde_size/2` evaluations of a degree-`<n` polynomial
1232+
/// on the g²-coset — to `2n` evaluations on the g-coset (the full LDE domain).
1233+
///
1234+
/// Fused: iFFT(n) → coset reshift g²→g → forward FFT(2n) in a single pass with no
1235+
/// intermediate coefficient `Polynomial`. The twiddles and the weights `g⁻ʲ/n`
1236+
/// (which fold the 1/n normalization and the net g²→g shift) are cached lazily
1237+
/// once per domain in [`LdeTwiddles`].
11761238
fn extend_half_to_lde(
11771239
half_evals: &[FieldElement<FieldExtension>],
1178-
squared_offset: &FieldElement<Field>,
1179-
domain: &Domain<Field>,
1240+
twiddles: &CompositionLdeTwiddles<Field>,
11801241
) -> Vec<FieldElement<FieldExtension>>
11811242
where
11821243
FieldElement<Field>: AsBytes,
11831244
FieldElement<FieldExtension>: AsBytes,
11841245
{
1185-
// iFFT on the N-point squared coset to get coefficients
1186-
let poly = Polynomial::interpolate_offset_fft(half_evals, squared_offset)
1187-
.expect("iFFT should succeed");
1188-
// Evaluate on the full LDE domain (2N points on the g-coset)
1189-
evaluate_polynomial_on_lde_domain(
1190-
&poly,
1191-
domain.blowup_factor,
1192-
domain.interpolation_domain_size,
1193-
&domain.coset_offset,
1246+
debug_assert_eq!(half_evals.len(), twiddles.weights.len());
1247+
Polynomial::coset_lde_full::<Field>(
1248+
half_evals,
1249+
2,
1250+
&twiddles.weights,
1251+
&twiddles.inv,
1252+
&twiddles.fwd,
11941253
)
1195-
.expect("LDE evaluation should succeed")
1254+
.expect("coset extension")
11961255
}
11971256

11981257
/// Returns the result of the second round of the STARK Prove protocol.
11991258
fn round_2_compute_composition_polynomial(
12001259
air: &dyn AIR<Field = Field, FieldExtension = FieldExtension, PublicInputs = PI>,
12011260
pub_inputs: &PI,
12021261
domain: &Domain<Field>,
1262+
twiddles: &LdeTwiddles<Field>,
12031263
round_1_result: &Round1<Field, FieldExtension>,
12041264
transition_coefficients: &[FieldElement<FieldExtension>],
12051265
boundary_coefficients: &[FieldElement<FieldExtension>],
@@ -1242,7 +1302,7 @@ pub trait IsStarkProver<
12421302
// H₀(x²) = (H(x) + H(-x)) / 2
12431303
// H₁(x²) = (H(x) - H(-x)) / (2x)
12441304
// On the LDE coset {g·ω^i}, we have -g·ω^i = g·ω^{i+N} since ω^N = -1.
1245-
Self::decompose_and_extend_d2(&constraint_evaluations, domain)
1305+
Self::decompose_and_extend_d2(&constraint_evaluations, domain, twiddles)
12461306
} else if number_of_parts == 1 {
12471307
// Degree bound equals trace length: constraint evals are the LDE directly.
12481308
vec![constraint_evaluations]
@@ -2373,6 +2433,7 @@ pub trait IsStarkProver<
23732433
&round_1_result,
23742434
table_transcript,
23752435
domain,
2436+
&twiddle_caches[idx],
23762437
)?;
23772438

23782439
#[cfg(feature = "instruments")]
@@ -2460,6 +2521,7 @@ pub trait IsStarkProver<
24602521
round_1_result: &Round1<Field, FieldExtension>,
24612522
transcript: &mut (impl IsStarkTranscript<FieldExtension, Field> + Clone),
24622523
domain: &Domain<Field>,
2524+
twiddles: &LdeTwiddles<Field>,
24632525
) -> Result<StarkProof<Field, FieldExtension, PI>, ProvingError>
24642526
where
24652527
FieldElement<Field>: AsBytes,
@@ -2500,6 +2562,7 @@ pub trait IsStarkProver<
25002562
air,
25012563
pub_inputs,
25022564
domain,
2565+
twiddles,
25032566
round_1_result,
25042567
&transition_coefficients,
25052568
&boundary_coefficients,

crypto/stark/src/tests/prover_tests.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
simple_fibonacci::{self, FibonacciAIR, FibonacciPublicInputs},
88
},
99
proof::options::ProofOptions,
10-
prover::{IsStarkProver, Prover, evaluate_polynomial_on_lde_domain},
10+
prover::{IsStarkProver, LdeTwiddles, Prover, evaluate_polynomial_on_lde_domain},
1111
test_utils::multi_prove_ram,
1212
tests::domain_cache_stats,
1313
tests::trace_test_helpers::get_trace_evaluations,
@@ -22,6 +22,42 @@ use math::{
2222

2323
type Felt = FieldElement<GoldilocksField>;
2424

25+
/// The fused composition half-extension (`extend_half_to_lde`) must produce exactly
26+
/// the same g-coset evaluations as the reference it replaces: iFFT on the g²-coset →
27+
/// coefficients → evaluate on the g-coset LDE. Both yield the unique degree-`<n`
28+
/// polynomial evaluated on the g-coset of size `2n`, so they must be byte-identical.
29+
#[test]
30+
fn composition_extend_half_fused_matches_reference() {
31+
use math::fft::bowers_fft::LayerTwiddles;
32+
type F = GoldilocksField;
33+
34+
let g = Felt::from(3); // any non-zero coset offset
35+
let g2 = &g * &g;
36+
37+
for n in [4usize, 8, 16, 32] {
38+
let half: Vec<Felt> = (0..n).map(|i| Felt::from((i as u64) * 7 + 1)).collect();
39+
40+
// Reference: iFFT(g²) → coeffs → evaluate on the g-coset of size 2n.
41+
let poly = Polynomial::interpolate_offset_fft(&half, &g2).unwrap();
42+
let reference = evaluate_polynomial_on_lde_domain(&poly, 2, n, &g).unwrap();
43+
44+
// Fused: coset_lde_full with weights wⱼ = g⁻ʲ / n.
45+
let n_inv = Felt::from(n as u64).inv().unwrap();
46+
let g_inv = g.inv().unwrap();
47+
let mut weights = Vec::with_capacity(n);
48+
let mut w = n_inv;
49+
for _ in 0..n {
50+
weights.push(w);
51+
w = &w * &g_inv;
52+
}
53+
let inv = LayerTwiddles::<F>::new_inverse(n.trailing_zeros() as u64).unwrap();
54+
let fwd = LayerTwiddles::<F>::new((2 * n).trailing_zeros() as u64).unwrap();
55+
let fused = Polynomial::coset_lde_full::<F>(&half, 2, &weights, &inv, &fwd).unwrap();
56+
57+
assert_eq!(reference, fused, "mismatch at n={n}");
58+
}
59+
}
60+
2561
#[test]
2662
fn test_domain_constructor() {
2763
let trace = simple_fibonacci::fibonacci_trace([Felt::from(1), Felt::from(1)], 8);
@@ -232,10 +268,15 @@ fn test_decompose_and_extend_d2_matches_original() {
232268
.collect();
233269

234270
// --- New path: algebraic decomposition ---
271+
let twiddles = LdeTwiddles::new(&domain);
272+
assert!(!twiddles.has_composition_cache());
235273
let new_result = Prover::<GoldilocksField, GoldilocksField, ()>::decompose_and_extend_d2(
236274
&constraint_evaluations,
237275
&domain,
276+
&twiddles,
238277
);
278+
#[cfg(not(feature = "cuda"))]
279+
assert!(twiddles.has_composition_cache());
239280

240281
assert_eq!(new_result.len(), 2);
241282
assert_eq!(new_result[0].len(), original[0].len());

0 commit comments

Comments
 (0)