11use std:: marker:: PhantomData ;
2- use std:: sync:: Arc ;
2+ use std:: sync:: { Arc , OnceLock } ;
33#[ cfg( feature = "instruments" ) ]
44use std:: time:: { Duration , Instant } ;
55
66use crypto:: fiat_shamir:: is_transcript:: IsStarkTranscript ;
77use math:: fft:: bit_reversing:: { in_place_bit_reverse_permute, reverse_index} ;
8- #[ cfg( any( test, feature = "test-utils" , feature = "debug-checks" ) ) ]
98use math:: fft:: bowers_fft:: LayerTwiddles ;
109use math:: fft:: errors:: FFTError ;
1110use 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
297340impl < 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,
0 commit comments