Skip to main content

p3_monty_31/dft/
mod.rs

1//! An implementation of the FFT for `MontyField31`
2extern crate alloc;
3
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6
7use itertools::izip;
8use p3_dft::TwoAdicSubgroupDft;
9use p3_field::{Field, PackedValue, PrimeCharacteristicRing};
10use p3_matrix::Matrix;
11use p3_matrix::bitrev::{BitReversedMatrixView, BitReversibleMatrix};
12use p3_matrix::dense::RowMajorMatrix;
13use p3_maybe_rayon::prelude::*;
14use p3_util::{log2_ceil_usize, log2_strict_usize};
15use spin::RwLock;
16use tracing::{debug_span, instrument};
17
18mod backward;
19mod forward;
20
21use crate::{FieldParameters, MontyField31, MontyParameters, TwoAdicData};
22
23/// Multiply each element of column `j` of `mat` by `shift**j`.
24#[instrument(level = "debug", skip_all)]
25fn coset_shift_and_scale_rows<F: Field>(
26    out: &mut [F],
27    out_ncols: usize,
28    mat: &[F],
29    ncols: usize,
30    shift: F,
31    scale: F,
32) {
33    debug_assert!(out.len().is_multiple_of(out_ncols));
34    debug_assert!(mat.len().is_multiple_of(ncols));
35    debug_assert!(out_ncols >= ncols);
36    debug_assert_eq!(out.len() / out_ncols, mat.len() / ncols);
37    let powers = shift.shifted_powers(scale).collect_n(ncols);
38    // Pack the shared per-column weights once; every row reuses the same split.
39    let (powers_packed, powers_suffix) = F::Packing::pack_slice_with_suffix(&powers);
40    out.par_chunks_exact_mut(out_ncols)
41        .zip(mat.par_chunks_exact(ncols))
42        .for_each(|(out_row, in_row)| {
43            // Only the first `ncols` entries carry data; the rest stays zero-padded.
44            let (out_packed, out_suffix) =
45                F::Packing::pack_slice_with_suffix_mut(&mut out_row[..ncols]);
46            let (in_packed, in_suffix) = F::Packing::pack_slice_with_suffix(in_row);
47            izip!(out_packed.iter_mut(), in_packed, powers_packed)
48                .for_each(|(out, &coeff, &weight)| *out = coeff * weight);
49            izip!(out_suffix.iter_mut(), in_suffix, powers_suffix)
50                .for_each(|(out, &coeff, &weight)| *out = coeff * weight);
51        });
52}
53
54/// Paired twiddle and inverse-twiddle tables, always updated atomically
55/// under a single lock to prevent concurrent observers from seeing a
56/// half-updated state.
57#[derive(Clone, Debug)]
58struct TwiddlePair<F> {
59    twiddles: Arc<[Vec<F>]>,
60    inv_twiddles: Arc<[Vec<F>]>,
61}
62
63impl<F> Default for TwiddlePair<F> {
64    fn default() -> Self {
65        Self {
66            twiddles: Arc::from(Vec::new()),
67            inv_twiddles: Arc::from(Vec::new()),
68        }
69    }
70}
71
72/// Recursive DFT, decimation-in-frequency in the forward direction,
73/// decimation-in-time in the backward (inverse) direction.
74#[derive(Clone, Debug, Default)]
75pub struct RecursiveDft<F> {
76    /// Memoized twiddle factors, paired with their inverses.
77    ///
78    /// Both tables are stored behind a single lock so they are always
79    /// updated atomically.
80    cache: Arc<RwLock<TwiddlePair<F>>>,
81}
82
83impl<MP: FieldParameters + TwoAdicData> RecursiveDft<MontyField31<MP>> {
84    pub fn new(n: usize) -> Self {
85        let res = Self::default();
86        res.update_twiddles(n);
87        res
88    }
89
90    #[inline]
91    fn decimation_in_freq_dft(
92        mat: &mut [MontyField31<MP>],
93        ncols: usize,
94        twiddles: &[Vec<MontyField31<MP>>],
95    ) {
96        if ncols > 1 {
97            let lg_fft_len = log2_strict_usize(ncols);
98            let twiddles = &twiddles[..(lg_fft_len - 1)];
99
100            mat.par_chunks_exact_mut(ncols)
101                .for_each(|v| MontyField31::forward_fft(v, twiddles));
102        }
103    }
104
105    #[inline]
106    fn decimation_in_time_dft(
107        mat: &mut [MontyField31<MP>],
108        ncols: usize,
109        twiddles: &[Vec<MontyField31<MP>>],
110    ) {
111        if ncols > 1 {
112            let lg_fft_len = p3_util::log2_strict_usize(ncols);
113            let twiddles = &twiddles[..(lg_fft_len - 1)];
114
115            mat.par_chunks_exact_mut(ncols)
116                .for_each(|v| MontyField31::backward_fft(v, twiddles));
117        }
118    }
119
120    /// Compute twiddle factors, or take memoized ones if already available.
121    #[instrument(skip_all)]
122    fn update_twiddles(&self, fft_len: usize) {
123        // As we don't save the twiddles for the final layer where
124        // the only twiddle is 1, roots_of_unity_table(fft_len)
125        // returns a vector of twiddles of length log_2(fft_len) - 1.
126        let need = log2_strict_usize(fft_len);
127
128        // Fast path: read lock to check if we already have enough.
129        let have = self.cache.read().twiddles.len() + 1;
130        if have >= need {
131            return;
132        }
133
134        let missing_twiddles = MontyField31::get_missing_twiddles(need, have);
135
136        let missing_inv_twiddles = missing_twiddles
137            .iter()
138            .map(|ts| {
139                core::iter::once(MontyField31::ONE)
140                    .chain(
141                        ts[1..]
142                            .iter()
143                            .rev()
144                            .map(|&t| MontyField31::new_monty(MP::PRIME - t.value)),
145                    )
146                    .collect()
147            })
148            .collect::<Vec<_>>();
149
150        // Slow path: acquire write lock and update both tables atomically.
151        let have_minus_one = have - 1;
152        let mut cache = self.cache.write();
153        let current_len = cache.twiddles.len();
154        // Double-check if an update is still needed after acquiring the write lock.
155        if (current_len + 1) < need {
156            let extend_from = current_len.saturating_sub(have_minus_one);
157
158            let mut tw = cache.twiddles.to_vec();
159            tw.extend_from_slice(&missing_twiddles[extend_from..]);
160
161            let mut inv_tw = cache.inv_twiddles.to_vec();
162            inv_tw.extend_from_slice(&missing_inv_twiddles[extend_from..]);
163
164            cache.twiddles = tw.into();
165            cache.inv_twiddles = inv_tw.into();
166        }
167    }
168
169    fn get_twiddles(&self) -> Arc<[Vec<MontyField31<MP>>]> {
170        self.cache.read().twiddles.clone()
171    }
172
173    fn get_inv_twiddles(&self) -> Arc<[Vec<MontyField31<MP>>]> {
174        self.cache.read().inv_twiddles.clone()
175    }
176}
177
178/// DFT implementation that uses DIT for the inverse "backward"
179/// direction and DIF for the "forward" direction.
180///
181/// The API mandates that the LDE is applied column-wise on the
182/// _row-major_ input. This is awkward for memory coherence, so the
183/// algorithm here transposes the input and operates on the rows in
184/// the typical way, then transposes back again for the output. Even
185/// for modestly large inputs, the cost of the two transposes
186/// outweighed by the improved performance from operating row-wise.
187///
188/// The choice of DIT for inverse and DIF for "forward" transform mean
189/// that a (coset) LDE
190///
191/// - IDFT / zero extend / DFT
192///
193/// expands to
194///
195///   - bit-reverse input
196///   - invDFT DIT
197///     - result is in "correct" order
198///   - coset shift and zero extend result
199///   - DFT DIF on result
200///     - output is bit-reversed, as required for FRI.
201///
202/// Hence the only bit-reversal that needs to take place is on the input.
203///
204impl<MP: MontyParameters + FieldParameters + TwoAdicData> TwoAdicSubgroupDft<MontyField31<MP>>
205    for RecursiveDft<MontyField31<MP>>
206{
207    type Evaluations = BitReversedMatrixView<RowMajorMatrix<MontyField31<MP>>>;
208
209    #[instrument(skip_all, fields(dims = %mat.dimensions(), added_bits))]
210    fn dft_batch(&self, mut mat: RowMajorMatrix<MontyField31<MP>>) -> Self::Evaluations
211    where
212        MP: MontyParameters + FieldParameters + TwoAdicData,
213    {
214        let nrows = mat.height();
215        let ncols = mat.width();
216
217        if nrows <= 1 {
218            return mat.bit_reverse_rows();
219        }
220
221        let mut scratch = debug_span!("allocate scratch space")
222            .in_scope(|| RowMajorMatrix::default(nrows, ncols));
223
224        self.update_twiddles(nrows);
225        let twiddles = self.get_twiddles();
226
227        // transpose input
228        debug_span!("pre-transpose", nrows, ncols).in_scope(|| {
229            p3_util::transpose::transpose(&mat.values, &mut scratch.values, ncols, nrows);
230        });
231
232        debug_span!("dft batch", n_dfts = ncols, fft_len = nrows)
233            .in_scope(|| Self::decimation_in_freq_dft(&mut scratch.values, nrows, &twiddles));
234
235        // transpose output
236        debug_span!("post-transpose", nrows = ncols, ncols = nrows).in_scope(|| {
237            p3_util::transpose::transpose(&scratch.values, &mut mat.values, nrows, ncols);
238        });
239
240        mat.bit_reverse_rows()
241    }
242
243    #[instrument(skip_all, fields(dims = %mat.dimensions(), added_bits))]
244    fn idft_batch(&self, mat: RowMajorMatrix<MontyField31<MP>>) -> RowMajorMatrix<MontyField31<MP>>
245    where
246        MP: MontyParameters + FieldParameters + TwoAdicData,
247    {
248        let nrows = mat.height();
249        let ncols = mat.width();
250        if nrows <= 1 {
251            return mat;
252        }
253
254        let mut scratch = debug_span!("allocate scratch space")
255            .in_scope(|| RowMajorMatrix::default(nrows, ncols));
256
257        let mut mat =
258            debug_span!("initial bitrev").in_scope(|| mat.bit_reverse_rows().to_row_major_matrix());
259
260        self.update_twiddles(nrows);
261        let inv_twiddles = self.get_inv_twiddles();
262
263        // transpose input
264        debug_span!("pre-transpose", nrows, ncols).in_scope(|| {
265            p3_util::transpose::transpose(&mat.values, &mut scratch.values, ncols, nrows);
266        });
267
268        debug_span!("idft", n_dfts = ncols, fft_len = nrows)
269            .in_scope(|| Self::decimation_in_time_dft(&mut scratch.values, nrows, &inv_twiddles));
270
271        // transpose output
272        debug_span!("post-transpose", nrows = ncols, ncols = nrows).in_scope(|| {
273            p3_util::transpose::transpose(&scratch.values, &mut mat.values, nrows, ncols);
274        });
275
276        let log_rows = log2_ceil_usize(nrows);
277        let inv_len = MontyField31::ONE.div_2exp_u64(log_rows as u64);
278        debug_span!("scale").in_scope(|| mat.scale(inv_len));
279        mat
280    }
281
282    #[instrument(skip_all, level = "debug", fields(dims = %mat.dimensions(), added_bits))]
283    fn coset_lde_batch(
284        &self,
285        mat: RowMajorMatrix<MontyField31<MP>>,
286        added_bits: usize,
287        shift: MontyField31<MP>,
288    ) -> Self::Evaluations {
289        let nrows = mat.height();
290        let ncols = mat.width();
291        let result_nrows = nrows << added_bits;
292
293        if nrows == 1 {
294            let dupd_rows = core::iter::repeat_n(mat.values, result_nrows)
295                .flatten()
296                .collect();
297            return RowMajorMatrix::new(dupd_rows, ncols).bit_reverse_rows();
298        }
299
300        let input_size = nrows * ncols;
301        let output_size = result_nrows * ncols;
302
303        let mat = mat.bit_reverse_rows().to_row_major_matrix();
304
305        // Allocate space for the output and the intermediate state.
306        let (mut output, mut padded) = debug_span!("allocate scratch space").in_scope(|| {
307            // Safety: These are pretty dodgy, but work because MontyField31 is #[repr(transparent)]
308            let output = MontyField31::<MP>::zero_vec(output_size);
309            let padded = MontyField31::<MP>::zero_vec(output_size);
310            (output, padded)
311        });
312
313        // `coeffs` will hold the result of the inverse FFT; use the
314        // output storage as scratch space.
315        let coeffs = &mut output[..input_size];
316
317        debug_span!("pre-transpose", nrows, ncols)
318            .in_scope(|| p3_util::transpose::transpose(&mat.values, coeffs, ncols, nrows));
319
320        // Apply inverse DFT; result is not yet normalised.
321        self.update_twiddles(result_nrows);
322        let inv_twiddles = self.get_inv_twiddles();
323        debug_span!("inverse dft batch", n_dfts = ncols, fft_len = nrows)
324            .in_scope(|| Self::decimation_in_time_dft(coeffs, nrows, &inv_twiddles));
325
326        // At this point the inverse FFT of each column of `mat` appears
327        // as a row in `coeffs`.
328
329        // Normalise inverse DFT and coset shift in one go.
330        let log_rows = log2_ceil_usize(nrows);
331        let inv_len = MontyField31::ONE.div_2exp_u64(log_rows as u64);
332        coset_shift_and_scale_rows(&mut padded, result_nrows, coeffs, nrows, shift, inv_len);
333
334        // `padded` is implicitly zero padded since it was initialised
335        // to zeros when declared above.
336
337        let twiddles = self.get_twiddles();
338
339        // Apply DFT
340        debug_span!("dft batch", n_dfts = ncols, fft_len = result_nrows)
341            .in_scope(|| Self::decimation_in_freq_dft(&mut padded, result_nrows, &twiddles));
342
343        // transpose output
344        debug_span!("post-transpose", nrows = ncols, ncols = result_nrows)
345            .in_scope(|| p3_util::transpose::transpose(&padded, &mut output, result_nrows, ncols));
346
347        RowMajorMatrix::new(output, ncols).bit_reverse_rows()
348    }
349}