Skip to main content

p3_dft/
butterflies.rs

1use core::mem::MaybeUninit;
2
3use itertools::izip;
4use p3_field::{Field, PackedField, PackedValue};
5
6/// A butterfly operation used in NTT to combine two values into a new pair.
7///
8/// This trait defines how to transform two elements (or vectors of elements)
9/// according to the structure of a butterfly gate.
10///
11/// In an NTT, butterflies are the core units that recursively combine values
12/// across layers. Each butterfly computes:
13/// ```text
14///   (a + b * twiddle, a - b * twiddle)   // DIT
15/// or
16///   (a + b, (a - b) * twiddle)           // DIF
17/// ```
18/// The transformation can be applied:
19/// - in-place (mutating input values)
20/// - to full rows of values (arrays of field elements)
21/// - out-of-place (writing results to separate destination buffers)
22///
23/// Different butterfly variants (DIT, DIF, or twiddle-free) define the exact formula.
24pub trait Butterfly<F: Field>: Copy + Send + Sync {
25    /// Applies the butterfly transformation to two packed field values.
26    ///
27    /// This method takes two inputs `x_1` and `x_2` and returns two outputs `(y_1, y_2)`
28    /// depending on the butterfly type.
29    /// ```text
30    /// Example (DIF):
31    ///   Input:  x_1 = a, x_2 = b
32    ///   Output: (a + b, (a - b) * twiddle)
33    /// ```
34    fn apply<PF: PackedField<Scalar = F>>(&self, x_1: PF, x_2: PF) -> (PF, PF);
35
36    /// Applies the butterfly in-place to two packed values.
37    ///
38    /// Mutates both `x_1` and `x_2` directly, storing the result of `apply`.
39    #[inline]
40    fn apply_in_place<PF: PackedField<Scalar = F>>(&self, x_1: &mut PF, x_2: &mut PF) {
41        (*x_1, *x_2) = self.apply(*x_1, *x_2);
42    }
43
44    /// Applies the butterfly transformation to two rows of scalar field values.
45    ///
46    /// Each row is a slice of `F`. This function processes the rows in packed
47    /// chunks using SIMD where possible, and falls back to scalar operations
48    /// for the suffix (remaining elements).
49    ///
50    /// The transformation is done in-place.
51    #[inline]
52    fn apply_to_rows(&self, row_1: &mut [F], row_2: &mut [F]) {
53        let (shorts_1, suffix_1) = F::Packing::pack_slice_with_suffix_mut(row_1);
54        let (shorts_2, suffix_2) = F::Packing::pack_slice_with_suffix_mut(row_2);
55        debug_assert_eq!(shorts_1.len(), shorts_2.len());
56        debug_assert_eq!(suffix_1.len(), suffix_2.len());
57        for (x_1, x_2) in shorts_1.iter_mut().zip(shorts_2) {
58            self.apply_in_place(x_1, x_2);
59        }
60        for (x_1, x_2) in suffix_1.iter_mut().zip(suffix_2) {
61            self.apply_in_place(x_1, x_2);
62        }
63    }
64
65    /// Applies the butterfly out-of-place to two source rows.
66    ///
67    /// This version does not overwrite the source. Instead, it writes the
68    /// result of each butterfly to separate destination slices (which may
69    /// be uninitialized memory).
70    ///
71    /// This is useful when performing LDE's where the size of the output is larger than the size of the input.
72    ///
73    /// - `src_1`, `src_2`: input slices
74    /// - `dst_1`, `dst_2`: output slices to write to (must be MaybeUninit)
75    #[inline]
76    fn apply_to_rows_oop(
77        &self,
78        src_1: &[F],
79        dst_1: &mut [MaybeUninit<F>],
80        src_2: &[F],
81        dst_2: &mut [MaybeUninit<F>],
82    ) {
83        let (src_shorts_1, src_suffix_1) = F::Packing::pack_slice_with_suffix(src_1);
84        let (src_shorts_2, src_suffix_2) = F::Packing::pack_slice_with_suffix(src_2);
85        let (dst_shorts_1, dst_suffix_1) =
86            F::Packing::pack_maybe_uninit_slice_with_suffix_mut(dst_1);
87        let (dst_shorts_2, dst_suffix_2) =
88            F::Packing::pack_maybe_uninit_slice_with_suffix_mut(dst_2);
89        debug_assert_eq!(src_shorts_1.len(), src_shorts_2.len());
90        debug_assert_eq!(src_suffix_1.len(), src_suffix_2.len());
91        debug_assert_eq!(dst_shorts_1.len(), dst_shorts_2.len());
92        debug_assert_eq!(dst_suffix_1.len(), dst_suffix_2.len());
93        for (s_1, s_2, d_1, d_2) in izip!(src_shorts_1, src_shorts_2, dst_shorts_1, dst_shorts_2) {
94            let (res_1, res_2) = self.apply(*s_1, *s_2);
95            d_1.write(res_1);
96            d_2.write(res_2);
97        }
98        for (s_1, s_2, d_1, d_2) in izip!(src_suffix_1, src_suffix_2, dst_suffix_1, dst_suffix_2) {
99            let (res_1, res_2) = self.apply(*s_1, *s_2);
100            d_1.write(res_1);
101            d_2.write(res_2);
102        }
103    }
104}
105
106/// DIF (Decimation-In-Frequency) butterfly operation.
107///
108/// Used in the *output-ordering* variant of NTT.
109/// This butterfly computes:
110/// ```text
111///   output_1 = x1 + x2
112///   output_2 = (x1 - x2) * twiddle
113/// ```
114/// The twiddle factor is applied after subtraction.
115/// Suitable for DIF-style recursive transforms.
116#[derive(Copy, Clone)]
117#[repr(transparent)] // Allows safe transmutes from F to this.
118pub struct DifButterfly<F>(pub F);
119
120impl<F: Field> Butterfly<F> for DifButterfly<F> {
121    #[inline]
122    fn apply<PF: PackedField<Scalar = F>>(&self, x_1: PF, x_2: PF) -> (PF, PF) {
123        (x_1 + x_2, (x_1 - x_2) * self.0)
124    }
125}
126
127/// DIF (Decimation-In-Frequency) butterfly operation where `x_2` is guaranteed to be zero.
128///
129/// Useful in scenarios where the input has just been padded with zeros.
130///
131/// Used in the *output-ordering* variant of NTT.
132/// This butterfly computes:
133/// ```text
134///   output_1 = x1
135///   output_2 = x1 * twiddle
136/// ```
137#[derive(Copy, Clone)]
138#[repr(transparent)] // Allows safe transmutes from F to this.
139pub struct DifButterflyZeros<F>(pub F);
140
141impl<F: Field> Butterfly<F> for DifButterflyZeros<F> {
142    #[inline]
143    fn apply<PF: PackedField<Scalar = F>>(&self, x_1: PF, x_2: PF) -> (PF, PF) {
144        debug_assert!(x_2.as_slice().iter().all(|x| x.is_zero())); // Slightly convoluted but PF may not implement equality.
145        (x_1, x_1 * self.0)
146    }
147
148    #[inline]
149    fn apply_to_rows(&self, row_1: &mut [F], row_2: &mut [F]) {
150        let (shorts_1, suffix_1) = F::Packing::pack_slice_with_suffix(row_1);
151        let (shorts_2, suffix_2) = F::Packing::pack_slice_with_suffix_mut(row_2);
152        debug_assert_eq!(shorts_1.len(), shorts_2.len());
153        debug_assert_eq!(suffix_1.len(), suffix_2.len());
154        for (x_1, x_2) in shorts_1.iter().zip(shorts_2) {
155            debug_assert!(x_2.as_slice().iter().all(|x| x.is_zero())); // Slightly convoluted but PF may not implement equality.
156            *x_2 = *x_1 * self.0; // x_2 is guaranteed to be zero, so we just set it to x_1 * twiddle. 
157        }
158        for (x_1, x_2) in suffix_1.iter().zip(suffix_2) {
159            debug_assert!(x_2.is_zero());
160            *x_2 = *x_1 * self.0; // x_2 is guaranteed to be zero, so we just set it to x_1 * twiddle. 
161        }
162    }
163}
164
165/// DIT (Decimation-In-Time) butterfly operation.
166///
167/// Used in the *input-ordering* variant of NTT/FFT.
168/// This butterfly computes:
169/// ```text
170///   output_1 = x1 + x2 * twiddle
171///   output_2 = x1 - x2 * twiddle
172/// ```
173/// The twiddle factor is applied to x2 before combining.
174/// Suitable for DIT-style recursive transforms.
175#[derive(Copy, Clone)]
176#[repr(transparent)] // Allows safe transmutes from F to this.
177pub struct DitButterfly<F>(pub F);
178
179impl<F: Field> Butterfly<F> for DitButterfly<F> {
180    #[inline]
181    fn apply<PF: PackedField<Scalar = F>>(&self, x_1: PF, x_2: PF) -> (PF, PF) {
182        let x_2_twiddle = x_2 * self.0;
183        (x_1 + x_2_twiddle, x_1 - x_2_twiddle)
184    }
185
186    /// Override `apply_to_rows` to pre-broadcast the twiddle factor into a packed field
187    /// once before the inner loop, avoiding a scalar-to-vector broadcast on each packed
188    /// multiplication. For wide rows (e.g., 256 columns with AVX512 width=16, giving 16
189    /// packed iterations per row-pair), this eliminates 15 redundant broadcasts per call.
190    /// Manually unroll the inner packed loop to expose multiple independent mul chains
191    /// to the compiler's scheduler, hiding the ~12–15 cyc Montgomery mul latency.
192    #[inline]
193    fn apply_to_rows(&self, row_1: &mut [F], row_2: &mut [F]) {
194        let (shorts_1, suffix_1) = F::Packing::pack_slice_with_suffix_mut(row_1);
195        let (shorts_2, suffix_2) = F::Packing::pack_slice_with_suffix_mut(row_2);
196        debug_assert_eq!(shorts_1.len(), shorts_2.len());
197        debug_assert_eq!(suffix_1.len(), suffix_2.len());
198        let twiddle_packed = F::Packing::from(self.0);
199        let mut c1 = shorts_1.chunks_exact_mut(4);
200        let mut c2 = shorts_2.chunks_exact_mut(4);
201        for (p1, p2) in (&mut c1).zip(&mut c2) {
202            let a1 = p1[0];
203            let b1 = p1[1];
204            let c1_ = p1[2];
205            let d1 = p1[3];
206            let a2 = p2[0];
207            let b2 = p2[1];
208            let c2_ = p2[2];
209            let d2 = p2[3];
210            let a2t = a2 * twiddle_packed;
211            let b2t = b2 * twiddle_packed;
212            let c2t = c2_ * twiddle_packed;
213            let d2t = d2 * twiddle_packed;
214            p1[0] = a1 + a2t;
215            p2[0] = a1 - a2t;
216            p1[1] = b1 + b2t;
217            p2[1] = b1 - b2t;
218            p1[2] = c1_ + c2t;
219            p2[2] = c1_ - c2t;
220            p1[3] = d1 + d2t;
221            p2[3] = d1 - d2t;
222        }
223        for (x_1, x_2) in c1
224            .into_remainder()
225            .iter_mut()
226            .zip(c2.into_remainder().iter_mut())
227        {
228            let x_2_twiddle = *x_2 * twiddle_packed;
229            let new_x1 = *x_1 + x_2_twiddle;
230            *x_2 = *x_1 - x_2_twiddle;
231            *x_1 = new_x1;
232        }
233        for (x_1, x_2) in suffix_1.iter_mut().zip(suffix_2.iter_mut()) {
234            self.apply_in_place(x_1, x_2);
235        }
236    }
237
238    /// Out-of-place variant with matching unroll factor.
239    #[inline]
240    fn apply_to_rows_oop(
241        &self,
242        src_1: &[F],
243        dst_1: &mut [MaybeUninit<F>],
244        src_2: &[F],
245        dst_2: &mut [MaybeUninit<F>],
246    ) {
247        let (src_shorts_1, src_suffix_1) = F::Packing::pack_slice_with_suffix(src_1);
248        let (src_shorts_2, src_suffix_2) = F::Packing::pack_slice_with_suffix(src_2);
249        let (dst_shorts_1, dst_suffix_1) =
250            F::Packing::pack_maybe_uninit_slice_with_suffix_mut(dst_1);
251        let (dst_shorts_2, dst_suffix_2) =
252            F::Packing::pack_maybe_uninit_slice_with_suffix_mut(dst_2);
253        debug_assert_eq!(src_shorts_1.len(), src_shorts_2.len());
254        debug_assert_eq!(src_suffix_1.len(), src_suffix_2.len());
255        debug_assert_eq!(dst_shorts_1.len(), dst_shorts_2.len());
256        debug_assert_eq!(dst_suffix_1.len(), dst_suffix_2.len());
257        let twiddle_packed = F::Packing::from(self.0);
258        let n = src_shorts_1.len();
259        let n4 = n - (n & 3);
260        let mut i = 0;
261        while i < n4 {
262            let a1 = src_shorts_1[i];
263            let b1 = src_shorts_1[i + 1];
264            let c1 = src_shorts_1[i + 2];
265            let d1 = src_shorts_1[i + 3];
266            let a2 = src_shorts_2[i];
267            let b2 = src_shorts_2[i + 1];
268            let c2 = src_shorts_2[i + 2];
269            let d2 = src_shorts_2[i + 3];
270            let a2t = a2 * twiddle_packed;
271            let b2t = b2 * twiddle_packed;
272            let c2t = c2 * twiddle_packed;
273            let d2t = d2 * twiddle_packed;
274            dst_shorts_1[i].write(a1 + a2t);
275            dst_shorts_2[i].write(a1 - a2t);
276            dst_shorts_1[i + 1].write(b1 + b2t);
277            dst_shorts_2[i + 1].write(b1 - b2t);
278            dst_shorts_1[i + 2].write(c1 + c2t);
279            dst_shorts_2[i + 2].write(c1 - c2t);
280            dst_shorts_1[i + 3].write(d1 + d2t);
281            dst_shorts_2[i + 3].write(d1 - d2t);
282            i += 4;
283        }
284        while i < n {
285            let s1 = src_shorts_1[i];
286            let s2 = src_shorts_2[i];
287            let x_2_twiddle = s2 * twiddle_packed;
288            dst_shorts_1[i].write(s1 + x_2_twiddle);
289            dst_shorts_2[i].write(s1 - x_2_twiddle);
290            i += 1;
291        }
292        for (s_1, s_2, d_1, d_2) in izip!(src_suffix_1, src_suffix_2, dst_suffix_1, dst_suffix_2) {
293            let (res_1, res_2) = self.apply(*s_1, *s_2);
294            d_1.write(res_1);
295            d_2.write(res_2);
296        }
297    }
298}
299
300/// DIT (Decimation-In-Time) butterfly operation with a post-multiplication scale factor.
301///
302/// This butterfly computes:
303/// ```text
304///   output_1 = (x1 + x2 * twiddle) * scale
305///   output_2 = (x1 - x2 * twiddle) * scale
306/// ```
307/// which is equivalent to:
308/// ```text
309///   output_1 = x1 * scale + x2 * (twiddle * scale)
310///   output_2 = x1 * scale - x2 * (twiddle * scale)
311/// ```
312///
313/// This is used to merge a uniform scaling step (e.g., 1/N normalization in inverse DFT)
314/// into a butterfly pass, avoiding a separate memory pass over the data.
315///
316/// The struct stores `scale` and `twiddle_times_scale = twiddle * scale` so that the
317/// `apply` method only needs 2 multiplications instead of 3.
318#[derive(Copy, Clone)]
319pub struct ScaledDitButterfly<F> {
320    pub twiddle: F,
321    pub scale: F,
322    /// Precomputed product `twiddle * scale` to reduce multiplications in the hot loop.
323    pub twiddle_times_scale: F,
324}
325
326impl<F: Field> ScaledDitButterfly<F> {
327    /// Construct a `ScaledDitButterfly`, precomputing `twiddle * scale`.
328    #[inline]
329    pub fn new(twiddle: F, scale: F) -> Self {
330        Self {
331            twiddle,
332            scale,
333            twiddle_times_scale: twiddle * scale,
334        }
335    }
336}
337
338impl<F: Field> Butterfly<F> for ScaledDitButterfly<F> {
339    #[inline]
340    fn apply<PF: PackedField<Scalar = F>>(&self, x_1: PF, x_2: PF) -> (PF, PF) {
341        // 2 multiplications instead of 3:
342        //   x1_s   = x1 * scale
343        //   x2_ts  = x2 * (twiddle * scale)   [precomputed]
344        //   out1   = x1_s + x2_ts
345        //   out2   = x1_s - x2_ts
346        let x_1_scale = x_1 * self.scale;
347        let x_2_twiddle_scale = x_2 * self.twiddle_times_scale;
348        (x_1_scale + x_2_twiddle_scale, x_1_scale - x_2_twiddle_scale)
349    }
350
351    /// Override `apply_to_rows` to pre-broadcast both `scale` and `twiddle_times_scale`
352    /// into packed fields once before the inner loop.
353    #[inline]
354    fn apply_to_rows(&self, row_1: &mut [F], row_2: &mut [F]) {
355        let (shorts_1, suffix_1) = F::Packing::pack_slice_with_suffix_mut(row_1);
356        let (shorts_2, suffix_2) = F::Packing::pack_slice_with_suffix_mut(row_2);
357        debug_assert_eq!(shorts_1.len(), shorts_2.len());
358        debug_assert_eq!(suffix_1.len(), suffix_2.len());
359        let scale_packed = F::Packing::from(self.scale);
360        let twiddle_times_scale_packed = F::Packing::from(self.twiddle_times_scale);
361        // ScaledDitButterfly has 2 muls per butterfly (scale + twiddle_scale), so unroll-4
362        // exposes 8 independent mul chains — better ILP than unroll-2's 4 chains.
363        let mut c1 = shorts_1.chunks_exact_mut(4);
364        let mut c2 = shorts_2.chunks_exact_mut(4);
365        for (p1, p2) in (&mut c1).zip(&mut c2) {
366            let a1 = p1[0];
367            let b1 = p1[1];
368            let c1_ = p1[2];
369            let d1 = p1[3];
370            let a2 = p2[0];
371            let b2 = p2[1];
372            let c2_ = p2[2];
373            let d2 = p2[3];
374            let a1s = a1 * scale_packed;
375            let b1s = b1 * scale_packed;
376            let c1s = c1_ * scale_packed;
377            let d1s = d1 * scale_packed;
378            let a2t = a2 * twiddle_times_scale_packed;
379            let b2t = b2 * twiddle_times_scale_packed;
380            let c2t = c2_ * twiddle_times_scale_packed;
381            let d2t = d2 * twiddle_times_scale_packed;
382            p1[0] = a1s + a2t;
383            p2[0] = a1s - a2t;
384            p1[1] = b1s + b2t;
385            p2[1] = b1s - b2t;
386            p1[2] = c1s + c2t;
387            p2[2] = c1s - c2t;
388            p1[3] = d1s + d2t;
389            p2[3] = d1s - d2t;
390        }
391        for (x_1, x_2) in c1
392            .into_remainder()
393            .iter_mut()
394            .zip(c2.into_remainder().iter_mut())
395        {
396            let x_1_scale = *x_1 * scale_packed;
397            let x_2_twiddle_scale = *x_2 * twiddle_times_scale_packed;
398            *x_1 = x_1_scale + x_2_twiddle_scale;
399            *x_2 = x_1_scale - x_2_twiddle_scale;
400        }
401        for (x_1, x_2) in suffix_1.iter_mut().zip(suffix_2.iter_mut()) {
402            self.apply_in_place(x_1, x_2);
403        }
404    }
405}
406
407/// Butterfly with no twiddle factor (`twiddle = 1`).
408///
409/// This is used when no root-of-unity scaling is needed.
410/// It works for either DIT or DIF, and is often used at
411/// the final or base level of a transform tree.
412///
413/// This butterfly computes:
414/// ```text
415///   - output_1 = x1 + x2
416///   - output_2 = x1 - x2
417/// ```
418#[derive(Copy, Clone)]
419pub struct TwiddleFreeButterfly;
420
421impl<F: Field> Butterfly<F> for TwiddleFreeButterfly {
422    #[inline]
423    fn apply<PF: PackedField<Scalar = F>>(&self, x_1: PF, x_2: PF) -> (PF, PF) {
424        (x_1 + x_2, x_1 - x_2)
425    }
426}