Skip to main content

p3_matrix/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3
4extern crate alloc;
5
6use alloc::vec::Vec;
7use core::fmt::{Debug, Display, Formatter};
8use core::ops::Deref;
9
10use itertools::Itertools;
11use p3_field::{
12    BasedVectorSpace, ExtensionField, Field, FieldArray, PackedField, PackedFieldExtension,
13    PackedValue, PrimeCharacteristicRing,
14};
15use p3_maybe_rayon::PARALLEL_ENABLED;
16use p3_maybe_rayon::prelude::*;
17use strided::{VerticallyStridedMatrixView, VerticallyStridedRowIndexMap};
18use tracing::instrument;
19
20use crate::dense::RowMajorMatrix;
21
22pub mod bitrev;
23pub mod dense;
24pub mod extension;
25pub mod horizontally_truncated;
26pub mod interpolation;
27pub mod row_index_mapped;
28pub mod stack;
29pub mod strided;
30pub mod util;
31
32/// A simple struct representing the shape of a matrix.
33///
34/// The `Dimensions` type stores the number of columns (`width`) and rows (`height`)
35/// of a matrix. It is commonly used for querying and displaying matrix shapes.
36#[derive(Copy, Clone, PartialEq, Eq)]
37pub struct Dimensions {
38    /// Number of columns in the matrix.
39    pub width: usize,
40    /// Number of rows in the matrix.
41    pub height: usize,
42}
43
44impl Debug for Dimensions {
45    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
46        write!(f, "{}x{}", self.width, self.height)
47    }
48}
49
50impl Display for Dimensions {
51    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
52        write!(f, "{}x{}", self.width, self.height)
53    }
54}
55
56/// A generic trait for two-dimensional matrix-like data structures.
57///
58/// The `Matrix` trait provides a uniform interface for accessing rows, elements,
59/// and computing with matrices in both sequential and parallel contexts. It supports
60/// packing strategies for SIMD optimizations and interaction with extension fields.
61pub trait Matrix<T: Send + Sync + Clone>: Send + Sync {
62    /// Returns the number of columns in the matrix.
63    fn width(&self) -> usize;
64
65    /// Returns the number of rows in the matrix.
66    fn height(&self) -> usize;
67
68    /// Returns the dimensions (width, height) of the matrix.
69    fn dimensions(&self) -> Dimensions {
70        Dimensions {
71            width: self.width(),
72            height: self.height(),
73        }
74    }
75
76    // The methods:
77    // get, get_unchecked, row, row_unchecked, row_subseq_unchecked, row_slice, row_slice_unchecked, row_subslice_unchecked
78    // are all defined in a circular manner so you only need to implement a subset of them.
79    // In particular is is enough to implement just one of: row_unchecked, row_subseq_unchecked
80    //
81    // That being said, most implementations will want to implement several methods for performance reasons.
82
83    /// Returns the element at the given row and column.
84    ///
85    /// Returns `None` if either `r >= height()` or `c >= width()`.
86    #[inline]
87    fn get(&self, r: usize, c: usize) -> Option<T> {
88        (r < self.height() && c < self.width()).then(|| unsafe {
89            // Safety: Clearly `r < self.height()` and `c < self.width()`.
90            self.get_unchecked(r, c)
91        })
92    }
93
94    /// Returns the element at the given row and column.
95    ///
96    /// For a safe alternative, see [`Self::get`].
97    ///
98    /// # Safety
99    /// The caller must ensure that `r < self.height()` and `c < self.width()`.
100    /// Breaking any of these assumptions is considered undefined behaviour.
101    #[inline]
102    unsafe fn get_unchecked(&self, r: usize, c: usize) -> T {
103        unsafe { self.row_slice_unchecked(r)[c].clone() }
104    }
105
106    /// Returns an iterator over the elements of the `r`-th row.
107    ///
108    /// The iterator will have `self.width()` elements.
109    ///
110    /// Returns `None` if `r >= height()`.
111    #[inline]
112    fn row(
113        &self,
114        r: usize,
115    ) -> Option<impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync>> {
116        (r < self.height()).then(|| unsafe {
117            // Safety: Clearly `r < self.height()`.
118            self.row_unchecked(r)
119        })
120    }
121
122    /// Returns an iterator over the elements of the `r`-th row.
123    ///
124    /// The iterator will have `self.width()` elements.
125    ///
126    /// For a safe alternative, see [`Self::row`].
127    ///
128    /// # Safety
129    /// The caller must ensure that `r < self.height()`.
130    /// Breaking this assumption is considered undefined behaviour.
131    #[inline]
132    unsafe fn row_unchecked(
133        &self,
134        r: usize,
135    ) -> impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync> {
136        unsafe { self.row_subseq_unchecked(r, 0, self.width()) }
137    }
138
139    /// Returns an iterator over the elements of the `r`-th row from position `start` to `end`.
140    ///
141    /// When `start = 0` and `end = width()`, this is equivalent to [`Self::row_unchecked`].
142    ///
143    /// For a safe alternative, use [`Self::row`], along with the `skip` and `take` iterator methods.
144    ///
145    /// # Safety
146    /// The caller must ensure that `r < self.height()` and `start <= end <= self.width()`.
147    /// Breaking any of these assumptions is considered undefined behaviour.
148    #[inline]
149    unsafe fn row_subseq_unchecked(
150        &self,
151        r: usize,
152        start: usize,
153        end: usize,
154    ) -> impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync> {
155        unsafe {
156            self.row_unchecked(r)
157                .into_iter()
158                .skip(start)
159                .take(end - start)
160        }
161    }
162
163    /// Returns the elements of the `r`-th row as something which can be coerced to a slice.
164    ///
165    /// Returns `None` if `r >= height()`.
166    #[inline]
167    fn row_slice(&self, r: usize) -> Option<impl Deref<Target = [T]>> {
168        (r < self.height()).then(|| unsafe {
169            // Safety: Clearly `r < self.height()`.
170            self.row_slice_unchecked(r)
171        })
172    }
173
174    /// Returns the elements of the `r`-th row as something which can be coerced to a slice.
175    ///
176    /// For a safe alternative, see [`Self::row_slice`].
177    ///
178    /// # Safety
179    /// The caller must ensure that `r < self.height()`.
180    /// Breaking this assumption is considered undefined behaviour.
181    #[inline]
182    unsafe fn row_slice_unchecked(&self, r: usize) -> impl Deref<Target = [T]> {
183        unsafe { self.row_subslice_unchecked(r, 0, self.width()) }
184    }
185
186    /// Returns a subset of elements of the `r`-th row as something which can be coerced to a slice.
187    ///
188    /// When `start = 0` and `end = width()`, this is equivalent to [`Self::row_slice_unchecked`].
189    ///
190    /// For a safe alternative, see [`Self::row_slice`].
191    ///
192    /// # Safety
193    /// The caller must ensure that `r < self.height()` and `start <= end <= self.width()`.
194    /// Breaking any of these assumptions is considered undefined behaviour.
195    #[inline]
196    unsafe fn row_subslice_unchecked(
197        &self,
198        r: usize,
199        start: usize,
200        end: usize,
201    ) -> impl Deref<Target = [T]> {
202        unsafe {
203            self.row_subseq_unchecked(r, start, end)
204                .into_iter()
205                .collect_vec()
206        }
207    }
208
209    /// Returns an iterator over all rows in the matrix.
210    #[inline]
211    fn rows(&self) -> impl Iterator<Item = impl Iterator<Item = T>> + Send + Sync {
212        unsafe {
213            // Safety: `r` always satisfies `r < self.height()`.
214            (0..self.height()).map(move |r| self.row_unchecked(r).into_iter())
215        }
216    }
217
218    /// Returns a parallel iterator over all rows in the matrix.
219    #[inline]
220    fn par_rows(
221        &self,
222    ) -> impl IndexedParallelIterator<Item = impl Iterator<Item = T>> + Send + Sync {
223        unsafe {
224            // Safety: `r` always satisfies `r < self.height()`.
225            (0..self.height())
226                .into_par_iter()
227                .map(move |r| self.row_unchecked(r).into_iter())
228        }
229    }
230
231    /// Collect the elements of the rows `r` through `r + c`. If anything is larger than `self.height()`
232    /// simply wrap around to the beginning of the matrix.
233    fn wrapping_row_slices(&self, r: usize, c: usize) -> Vec<impl Deref<Target = [T]>> {
234        unsafe {
235            // Safety: Thank to the `%`, the rows index is always less than `self.height()`.
236            (0..c)
237                .map(|i| self.row_slice_unchecked((r + i) % self.height()))
238                .collect_vec()
239        }
240    }
241
242    /// Returns an iterator over the first row of the matrix.
243    ///
244    /// Returns None if `height() == 0`.
245    #[inline]
246    fn first_row(
247        &self,
248    ) -> Option<impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync>> {
249        self.row(0)
250    }
251
252    /// Returns an iterator over the last row of the matrix.
253    ///
254    /// Returns None if `height() == 0`.
255    #[inline]
256    fn last_row(
257        &self,
258    ) -> Option<impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync>> {
259        if self.height() == 0 {
260            None
261        } else {
262            // Safety: Clearly `self.height() - 1 < self.height()`.
263            unsafe { Some(self.row_unchecked(self.height() - 1)) }
264        }
265    }
266
267    /// Converts the matrix into a `RowMajorMatrix` by collecting all rows into a single vector.
268    fn to_row_major_matrix(self) -> RowMajorMatrix<T>
269    where
270        Self: Sized,
271        T: Clone,
272    {
273        RowMajorMatrix::new(self.rows().flatten().collect(), self.width())
274    }
275
276    /// Get a packed iterator over the `r`-th row.
277    ///
278    /// If the row length is not divisible by the packing width, the final elements
279    /// are returned as a base iterator with length `<= P::WIDTH - 1`.
280    ///
281    /// # Panics
282    /// Panics if `r >= height()`.
283    fn horizontally_packed_row<'a, P>(
284        &'a self,
285        r: usize,
286    ) -> (
287        impl Iterator<Item = P> + Send + Sync,
288        impl Iterator<Item = T> + Send + Sync,
289    )
290    where
291        P: PackedValue<Value = T>,
292        T: Clone + 'a,
293    {
294        assert!(r < self.height(), "Row index out of bounds.");
295        let num_packed = self.width() / P::WIDTH;
296        unsafe {
297            // Safety: We have already checked that `r < height()`.
298            let mut iter = self
299                .row_subseq_unchecked(r, 0, num_packed * P::WIDTH)
300                .into_iter();
301
302            // array::from_fn is guaranteed to always call in order.
303            let packed =
304                (0..num_packed).map(move |_| P::from_fn(|_| iter.next().unwrap_unchecked()));
305
306            let sfx = self
307                .row_subseq_unchecked(r, num_packed * P::WIDTH, self.width())
308                .into_iter();
309            (packed, sfx)
310        }
311    }
312
313    /// Get a packed iterator over the `r`-th row.
314    ///
315    /// If the row length is not divisible by the packing width, the final entry will be zero-padded.
316    ///
317    /// # Panics
318    /// Panics if `r >= height()`.
319    fn padded_horizontally_packed_row<'a, P>(
320        &'a self,
321        r: usize,
322    ) -> impl Iterator<Item = P> + Send + Sync
323    where
324        P: PackedValue<Value = T>,
325        T: Clone + Default + 'a,
326    {
327        let mut row_iter = self.row(r).expect("Row index out of bounds.").into_iter();
328        let num_elems = self.width().div_ceil(P::WIDTH);
329        // array::from_fn is guaranteed to always call in order.
330        (0..num_elems).map(move |_| P::from_fn(|_| row_iter.next().unwrap_or_default()))
331    }
332
333    /// Get a parallel iterator over all packed rows of the matrix.
334    ///
335    /// If the matrix width is not divisible by the packing width, the final elements
336    /// of each row are returned as a base iterator with length `<= P::WIDTH - 1`.
337    fn par_horizontally_packed_rows<'a, P>(
338        &'a self,
339    ) -> impl IndexedParallelIterator<
340        Item = (
341            impl Iterator<Item = P> + Send + Sync,
342            impl Iterator<Item = T> + Send + Sync,
343        ),
344    >
345    where
346        P: PackedValue<Value = T>,
347        T: Clone + 'a,
348    {
349        (0..self.height())
350            .into_par_iter()
351            .map(|r| self.horizontally_packed_row(r))
352    }
353
354    /// Get a parallel iterator over all packed rows of the matrix.
355    ///
356    /// If the matrix width is not divisible by the packing width, the final entry of each row will be zero-padded.
357    fn par_padded_horizontally_packed_rows<'a, P>(
358        &'a self,
359    ) -> impl IndexedParallelIterator<Item = impl Iterator<Item = P> + Send + Sync>
360    where
361        P: PackedValue<Value = T>,
362        T: Clone + Default + 'a,
363    {
364        (0..self.height())
365            .into_par_iter()
366            .map(|r| self.padded_horizontally_packed_row(r))
367    }
368
369    /// Pack together a collection of adjacent rows from the matrix.
370    ///
371    /// Returns an iterator whose i'th element is packing of the i'th element of the
372    /// rows r through r + P::WIDTH - 1. If we exceed the height of the matrix,
373    /// wrap around and include initial rows.
374    #[inline]
375    fn vertically_packed_row<P>(&self, r: usize) -> impl Iterator<Item = P>
376    where
377        T: Copy,
378        P: PackedValue<Value = T>,
379    {
380        // Precompute row slices once to minimize redundant calls and improve performance.
381        let rows = self.wrapping_row_slices(r, P::WIDTH);
382
383        // Using precomputed rows avoids repeatedly calling `row_slice`, which is costly.
384        (0..self.width()).map(move |c| P::from_fn(|i| rows[i][c]))
385    }
386
387    /// Pack together a collection of rows and "next" rows from the matrix.
388    ///
389    /// Returns a vector corresponding to 2 packed rows. The i'th element of the first
390    /// row contains the packing of the i'th element of the rows r through r + P::WIDTH - 1.
391    /// The i'th element of the second row contains the packing of the i'th element of the
392    /// rows r + step through r + step + P::WIDTH - 1. If at some point we exceed the
393    /// height of the matrix, wrap around and include initial rows.
394    #[inline]
395    fn vertically_packed_row_pair<P>(&self, r: usize, step: usize) -> Vec<P>
396    where
397        T: Copy,
398        P: PackedValue<Value = T>,
399    {
400        // Whilst it would appear that this can be replaced by two calls to vertically_packed_row
401        // tests seem to indicate that combining them in the same function is slightly faster.
402        // It's probably allowing the compiler to make some optimizations on the fly.
403
404        let rows = self.wrapping_row_slices(r, P::WIDTH);
405        let next_rows = self.wrapping_row_slices(r + step, P::WIDTH);
406
407        (0..self.width())
408            .map(|c| P::from_fn(|i| rows[i][c]))
409            .chain((0..self.width()).map(|c| P::from_fn(|i| next_rows[i][c])))
410            .collect_vec()
411    }
412
413    /// Returns a view over a vertically strided submatrix.
414    ///
415    /// The view selects rows using `r = offset + i * stride` for each `i`.
416    fn vertically_strided(self, stride: usize, offset: usize) -> VerticallyStridedMatrixView<Self>
417    where
418        Self: Sized,
419    {
420        VerticallyStridedRowIndexMap::new_view(self, stride, offset)
421    }
422
423    /// Compute Mᵀv, aka premultiply this matrix by the given vector,
424    /// aka scale each row by the corresponding entry in `v` and take the sum across rows.
425    /// `v` can be a vector of extension elements.
426    #[instrument(level = "debug", skip_all, fields(dims = %self.dimensions()))]
427    fn columnwise_dot_product<EF>(&self, v: &[EF]) -> Vec<EF>
428    where
429        T: Field,
430        EF: ExtensionField<T>,
431    {
432        assert_eq!(
433            v.len(),
434            self.height(),
435            "weight count must match matrix height"
436        );
437
438        // Below this many total elements, the rayon fork-join and SIMD-packing machinery
439        // costs more than the dot product itself; fall back to a plain scalar accumulation.
440        // Gating on total elements (rather than height alone) also covers wide-but-short
441        // matrices, where a per-row cost proportional to width still adds up.
442        const SMALL_ELEMS: usize = 256;
443        if self.height().saturating_mul(self.width()) <= SMALL_ELEMS {
444            let mut acc = EF::zero_vec(self.width());
445            for (row, &scale) in self.rows().zip(v) {
446                for (l, r) in acc.iter_mut().zip(row) {
447                    *l += scale * r;
448                }
449            }
450            return acc;
451        }
452
453        let packed_width = self.width().div_ceil(T::Packing::WIDTH);
454
455        // Avoid multi-worker scheduling for modest products while bounding both input
456        // traffic and per-call extension-coefficient work. Larger products, serial
457        // builds, and one-worker pools retain the existing reduction.
458        //
459        // The task-size model cannot place this gate, since the two arms are different
460        // kernels rather than one kernel cut two ways.
461        //
462        // The serial arm defers modular reductions across rows; the reduction below cannot.
463        //
464        // So on Zen 5 at 32 workers the serial arm stays ahead to between 80 and 340 us of
465        // priced work, where the model asks for 20.
466        const SERIAL_PACKED_ELEMS: usize = 4096;
467        const SERIAL_PACKED_COEFF_OPS: usize = 512;
468        if T::Packing::WIDTH > 1
469            && self.height() > 1
470            && self.height().saturating_mul(self.width()) <= SERIAL_PACKED_ELEMS
471            && self
472                .height()
473                .saturating_mul(packed_width)
474                .saturating_mul(EF::DIMENSION)
475                <= SERIAL_PACKED_COEFF_OPS
476            && PARALLEL_ENABLED
477            && current_num_threads() > 1
478        {
479            return serial_packed_columnwise_dot_product(self, v);
480        }
481
482        let packed_result = self
483            .par_padded_horizontally_packed_rows::<T::Packing>()
484            .zip(v)
485            .par_fold_reduce(
486                || EF::ExtensionPacking::zero_vec(packed_width),
487                |mut acc, (row, &scale)| {
488                    let scale: EF::ExtensionPacking = scale.into();
489                    acc.iter_mut().zip(row).for_each(|(l, r)| *l += scale * r);
490                    acc
491                },
492                |mut acc_l, acc_r| {
493                    acc_l.iter_mut().zip(&acc_r).for_each(|(l, r)| *l += *r);
494                    acc_l
495                },
496            );
497
498        EF::ExtensionPacking::to_ext_iter(packed_result)
499            .take(self.width())
500            .collect()
501    }
502
503    /// Compute Mᵀ · [v₀, v₁, ..., vₙ₋₁] for N weight vectors simultaneously.
504    ///
505    /// Computes `result[col][j] = Σᵣ M[r, col] · vⱼ[r]` for all columns and all j ∈ [0, N).
506    ///
507    /// Batching N weight vectors reduces memory bandwidth: each matrix row is loaded once
508    /// instead of N times. Uses SIMD packing (width W) to process W columns in parallel.
509    #[instrument(level = "debug", skip_all, fields(dims = %self.dimensions()))]
510    fn columnwise_dot_product_batched<EF, const N: usize>(
511        &self,
512        vs: &[FieldArray<EF, N>],
513    ) -> Vec<FieldArray<EF, N>>
514    where
515        T: Field,
516        EF: ExtensionField<T>,
517    {
518        assert_eq!(vs.len(), self.height());
519
520        let packed_width = self.width().div_ceil(T::Packing::WIDTH);
521        let height = self.height();
522
523        // Split the rows into a bounded number of contiguous chunks; each task runs the
524        // field's columnwise kernel serially over its chunk (letting it defer modular
525        // reductions across rows) and the per-task accumulators are summed at the end.
526        //
527        // The floor collapses the split to a single chunk on a small matrix.
528        //
529        // Below that size the work is not worth handing to another worker.
530        let row_bytes = columnwise_row_bytes::<T, EF>(self.width(), N);
531        // The floor is never zero, so the chunk length is always usable as a divisor.
532        let chunk_rows = height
533            .div_ceil((4 * current_num_threads()).clamp(1, height.max(1)))
534            .max(min_task_len(height, row_bytes));
535        let num_chunks = height.div_ceil(chunk_rows);
536
537        let packed_results: Vec<EF::ExtensionPacking> =
538            (0..num_chunks).into_par_iter().par_fold_reduce(
539                || EF::ExtensionPacking::zero_vec(packed_width * N),
540                |mut acc, chunk| {
541                    let rows = chunk * chunk_rows..((chunk + 1) * chunk_rows).min(height);
542                    T::batched_columnwise_dot_product::<EF, _, _, N>(
543                        &mut acc,
544                        rows.map(|r| {
545                            (
546                                self.padded_horizontally_packed_row::<T::Packing>(r),
547                                vs[r].0,
548                            )
549                        }),
550                    );
551                    acc
552                },
553                |mut acc_l, acc_r| {
554                    acc_l.iter_mut().zip(&acc_r).for_each(|(lj, rj)| *lj += *rj);
555                    acc_l
556                },
557            );
558
559        // Unpack: chunk[j].lane(i) → result[c·W + i][j] for column batch c
560        packed_results
561            .chunks(N)
562            .flat_map(|chunk| {
563                (0..T::Packing::WIDTH)
564                    .map(move |lane| FieldArray::from_fn(|j| chunk[j].extract(lane)))
565            })
566            .take(self.width())
567            .collect()
568    }
569
570    /// Compute the matrix vector product `M . vec`, aka take the dot product of each
571    /// row of `M` by `vec`. If the length of `vec` is longer than the width of `M`,
572    /// `vec` is truncated to the first `width()` elements.
573    ///
574    /// We make use of `PackedFieldExtension` to speed up computations. Thus `vec` is passed in as
575    /// a slice of `PackedFieldExtension` elements.
576    ///
577    /// # Panics
578    /// This function panics if the length of `vec` is less than `self.width().div_ceil(T::Packing::WIDTH)`.
579    fn rowwise_packed_dot_product<EF>(
580        &self,
581        vec: &[EF::ExtensionPacking],
582    ) -> impl IndexedParallelIterator<Item = EF>
583    where
584        T: Field,
585        EF: ExtensionField<T>,
586    {
587        // The length of a `padded_horizontally_packed_row` is `self.width().div_ceil(T::Packing::WIDTH)`.
588        assert!(vec.len() >= self.width().div_ceil(T::Packing::WIDTH));
589
590        // Instead of creating N intermediate ExtPacking products and summing them,
591        // we track D separate BasePacking accumulators (one per extension coefficient).
592        self.par_padded_horizontally_packed_rows::<T::Packing>()
593            .map(move |row_packed| {
594                // Get the extension dimension from the first vec element's coefficients
595                let d = <EF::ExtensionPacking as BasedVectorSpace<T::Packing>>::DIMENSION;
596
597                // Accumulate coefficient-wise: for each (v, r) pair, acc[i] += v.coefficient(i) * r
598                let coeff_accs = T::Packing::coeffwise_dot_product(
599                    d,
600                    vec.iter()
601                        .zip(row_packed)
602                        .map(|(v, r)| (v.as_basis_coefficients_slice(), r)),
603                );
604
605                // Construct the result ExtPacking from the accumulators and sum the coefficients.
606                let packed_result =
607                    EF::ExtensionPacking::from_basis_coefficients_fn(|i| coeff_accs[i]);
608                EF::ExtensionPacking::to_ext_iter([packed_result]).sum()
609            })
610    }
611}
612
613/// Extension widths one [`COLUMNWISE_MAC_LANES`]-column multiply-accumulate step is charged.
614///
615/// Measured on Zen 5 at four weight vectors, [`COLUMNWISE_MAC_LANES`] columns per step:
616///
617/// ```text
618///     columns   real bytes per row   widths per step
619///          16                  602               7.4
620///          64                 2235               7.5
621///         256                 8716               7.4
622///         512                17424               7.5
623/// ```
624const COLUMNWISE_MAC_WIDTHS: usize = 7;
625
626/// Columns one packed step carried on the host the figure above was measured on.
627///
628/// Seven widths over sixteen lanes is 0.74 ns per column.
629///
630/// A four-lane NEON host reproduces that per column, not per packed step.
631///
632/// So the arithmetic is charged per column; per packed element it would scale with the
633/// host's lane count and overcharge that build by about 3.5x.
634const COLUMNWISE_MAC_LANES: usize = 16;
635
636/// Bytes one matrix row moves when weighted into `weight_vectors` accumulators.
637///
638/// ```text
639///     traffic : one packed row, plus the weights it scales
640///     arith   : one multiply-accumulate per column, per weight vector
641/// ```
642///
643/// Pricing the row by traffic alone puts the gate seven times too high, leaving a
644/// compute-bound matrix serial: 64 columns by 512 rows costs 114 us against 19 us split.
645const fn columnwise_row_bytes<T, EF>(width: usize, weight_vectors: usize) -> usize
646where
647    T: Field,
648    EF: ExtensionField<T>,
649{
650    // The kernel walks whole packed words, so padding lanes are multiplied like any other.
651    //
652    // That rounding is the only place the host's lane count enters the price.
653    let columns = width.div_ceil(T::Packing::WIDTH) * T::Packing::WIDTH;
654
655    columns * size_of::<T>()
656        + weight_vectors * size_of::<EF>()
657        + (COLUMNWISE_MAC_WIDTHS * weight_vectors * columns * size_of::<EF>())
658            .div_ceil(COLUMNWISE_MAC_LANES)
659}
660
661#[inline(never)]
662fn serial_packed_columnwise_dot_product<F, EF, M>(matrix: &M, weights: &[EF]) -> Vec<EF>
663where
664    F: Field,
665    EF: ExtensionField<F>,
666    M: Matrix<F> + ?Sized,
667{
668    let mut acc = EF::ExtensionPacking::zero_vec(matrix.width().div_ceil(F::Packing::WIDTH));
669    F::batched_columnwise_dot_product::<EF, _, _, 1>(
670        &mut acc,
671        (0..matrix.height()).map(|r| {
672            (
673                matrix.padded_horizontally_packed_row::<F::Packing>(r),
674                [weights[r]],
675            )
676        }),
677    );
678    EF::ExtensionPacking::to_ext_iter(acc)
679        .take(matrix.width())
680        .collect()
681}
682
683#[cfg(test)]
684mod tests {
685    use alloc::vec::Vec;
686    use alloc::{format, vec};
687
688    use itertools::izip;
689    use p3_baby_bear::BabyBear;
690    use p3_field::PrimeCharacteristicRing;
691    use p3_field::extension::{BinomialExtensionField, CubicTrinomialExtensionField};
692    use p3_goldilocks::Goldilocks;
693    use p3_mersenne_31::{Mersenne31, QM31};
694    use rand::SeedableRng;
695    use rand::rngs::SmallRng;
696
697    use super::*;
698    use crate::bitrev::BitReversibleMatrix;
699    use crate::extension::FlatMatrixView;
700
701    fn patterned_matrix<F: Field>(height: usize, width: usize) -> RowMajorMatrix<F> {
702        RowMajorMatrix::new(
703            (0..height * width)
704                .map(|i| F::from_usize((i * 17 + 3) % 127))
705                .collect(),
706            width,
707        )
708    }
709
710    fn patterned_extension_matrix<F, EF>(height: usize, width: usize) -> RowMajorMatrix<EF>
711    where
712        F: Field,
713        EF: ExtensionField<F>,
714    {
715        RowMajorMatrix::new(
716            (0..height * width)
717                .map(|i| {
718                    EF::from_basis_coefficients_fn(|d| {
719                        F::from_usize((i * EF::DIMENSION + d + 1) % 127)
720                    })
721                })
722                .collect(),
723            width,
724        )
725    }
726
727    fn assert_columnwise_dot_product_matches_scalar<F, EF, M>(mat: &M)
728    where
729        F: Field,
730        EF: ExtensionField<F>,
731        M: Matrix<F>,
732    {
733        let weights: Vec<EF> = (0..mat.height())
734            .map(|r| {
735                EF::from_basis_coefficients_fn(|d| F::from_usize((r * EF::DIMENSION + d + 5) % 127))
736            })
737            .collect();
738        let expected: Vec<EF> = (0..mat.width())
739            .map(|c| {
740                (0..mat.height())
741                    .map(|r| weights[r] * mat.get(r, c).unwrap())
742                    .sum()
743            })
744            .collect();
745
746        assert_eq!(mat.columnwise_dot_product(&weights), expected);
747    }
748
749    fn assert_columnwise_dot_product_grid<F, EF>()
750    where
751        F: Field,
752        EF: ExtensionField<F>,
753    {
754        for height in [0, 1, 17, 32, 128, 1024] {
755            for width in [1, 3, 8, 17, 65] {
756                let mat = patterned_matrix::<F>(height, width);
757                assert_columnwise_dot_product_matches_scalar::<F, EF, _>(&mat);
758            }
759        }
760    }
761
762    #[test]
763    fn test_columnwise_dot_product() {
764        type F = BabyBear;
765        type EF = BinomialExtensionField<BabyBear, 4>;
766
767        let mut rng = SmallRng::seed_from_u64(1);
768        let m = RowMajorMatrix::<F>::rand(&mut rng, 1 << 8, 1 << 4);
769        let v = RowMajorMatrix::<EF>::rand(&mut rng, 1 << 8, 1).values;
770
771        let mut expected = EF::zero_vec(m.width());
772        for (row, &scale) in izip!(m.rows(), &v) {
773            for (l, r) in izip!(&mut expected, row) {
774                *l += scale * r;
775            }
776        }
777
778        assert_eq!(m.columnwise_dot_product(&v), expected);
779    }
780
781    #[test]
782    fn test_columnwise_dot_product_small_height() {
783        type F = BabyBear;
784        type EF = BinomialExtensionField<BabyBear, 4>;
785
786        let mut rng = SmallRng::seed_from_u64(2);
787
788        // Cover heights below, at, and just above the small-height serial threshold.
789        for height in [0, 1, 3, 16, 17] {
790            let m = RowMajorMatrix::<F>::rand(&mut rng, height, 1 << 4);
791            let v = RowMajorMatrix::<EF>::rand(&mut rng, height, 1).values;
792
793            let mut expected = EF::zero_vec(m.width());
794            for (row, &scale) in izip!(m.rows(), &v) {
795                for (l, r) in izip!(&mut expected, row) {
796                    *l += scale * r;
797                }
798            }
799
800            assert_eq!(m.columnwise_dot_product(&v), expected, "height = {height}");
801        }
802    }
803
804    #[test]
805    fn test_columnwise_dot_product_matches_scalar_across_extension_fields() {
806        type BabyBear4 = BinomialExtensionField<BabyBear, 4>;
807        type BabyBear5 = BinomialExtensionField<BabyBear, 5>;
808        type Goldilocks2 = BinomialExtensionField<Goldilocks, 2>;
809        type Goldilocks3 = CubicTrinomialExtensionField<Goldilocks>;
810        type Mersenne31_3 = BinomialExtensionField<Mersenne31, 3>;
811
812        assert_columnwise_dot_product_grid::<BabyBear, BabyBear4>();
813        assert_columnwise_dot_product_grid::<BabyBear, BabyBear5>();
814        assert_columnwise_dot_product_grid::<Goldilocks, Goldilocks2>();
815        assert_columnwise_dot_product_grid::<Goldilocks, Goldilocks3>();
816        assert_columnwise_dot_product_grid::<Mersenne31, QM31>();
817        assert_columnwise_dot_product_grid::<Mersenne31, Mersenne31_3>();
818    }
819
820    #[test]
821    fn test_columnwise_dot_product_matches_scalar_for_matrix_views() {
822        type BabyBear4 = BinomialExtensionField<BabyBear, 4>;
823        type Goldilocks3 = CubicTrinomialExtensionField<Goldilocks>;
824        type Mersenne31_3 = BinomialExtensionField<Mersenne31, 3>;
825
826        let mapped = patterned_matrix::<BabyBear>(32, 17).bit_reverse_rows();
827        assert_columnwise_dot_product_matches_scalar::<BabyBear, BabyBear4, _>(&mapped);
828
829        let flat = FlatMatrixView::<Goldilocks, Goldilocks3, _>::new(patterned_extension_matrix::<
830            Goldilocks,
831            Goldilocks3,
832        >(32, 3));
833        assert_columnwise_dot_product_matches_scalar::<Goldilocks, Goldilocks3, _>(&flat);
834
835        let mapped_flat = FlatMatrixView::<Mersenne31, Mersenne31_3, _>::new(
836            patterned_extension_matrix::<Mersenne31, Mersenne31_3>(128, 3).bit_reverse_rows(),
837        );
838        assert_columnwise_dot_product_matches_scalar::<Mersenne31, Mersenne31_3, _>(&mapped_flat);
839    }
840
841    #[test]
842    #[should_panic(expected = "weight count must match matrix height")]
843    fn test_columnwise_dot_product_rejects_short_weights() {
844        let mat = patterned_matrix::<BabyBear>(17, 17);
845        let weights = BinomialExtensionField::<BabyBear, 4>::zero_vec(16);
846        let _ = mat.columnwise_dot_product(&weights);
847    }
848
849    #[test]
850    #[should_panic(expected = "weight count must match matrix height")]
851    fn test_columnwise_dot_product_rejects_long_weights() {
852        let mat = patterned_matrix::<BabyBear>(17, 17);
853        let weights = BinomialExtensionField::<BabyBear, 4>::zero_vec(18);
854        let _ = mat.columnwise_dot_product(&weights);
855    }
856
857    #[test]
858    fn test_columnwise_dot_product_batched() {
859        type F = BabyBear;
860        type EF = BinomialExtensionField<BabyBear, 4>;
861
862        let mut rng = SmallRng::seed_from_u64(1);
863        let m = RowMajorMatrix::<F>::rand(&mut rng, 1 << 8, 1 << 4);
864        let v1 = RowMajorMatrix::<EF>::rand(&mut rng, 1 << 8, 1).values;
865        let v2 = RowMajorMatrix::<EF>::rand(&mut rng, 1 << 8, 1).values;
866
867        // Compute expected via two separate calls
868        let expected1 = m.columnwise_dot_product(&v1);
869        let expected2 = m.columnwise_dot_product(&v2);
870
871        // Compute via batched call - returns Vec<[EF; 2]> where result[col] = [dot1, dot2]
872        let vs: Vec<FieldArray<EF, 2>> = v1
873            .into_iter()
874            .zip(v2)
875            .map(|(a, b)| FieldArray([a, b]))
876            .collect();
877        let results = m.columnwise_dot_product_batched::<EF, 2>(&vs);
878
879        // Extract each point's results
880        let result1: Vec<EF> = results.iter().map(|r| r[0]).collect();
881        let result2: Vec<EF> = results.iter().map(|r| r[1]).collect();
882
883        assert_eq!(result1, expected1);
884        assert_eq!(result2, expected2);
885    }
886
887    // Mock implementation for testing purposes
888    struct MockMatrix {
889        data: Vec<Vec<u32>>,
890        width: usize,
891        height: usize,
892    }
893
894    impl Matrix<u32> for MockMatrix {
895        fn width(&self) -> usize {
896            self.width
897        }
898
899        fn height(&self) -> usize {
900            self.height
901        }
902
903        unsafe fn row_unchecked(
904            &self,
905            r: usize,
906        ) -> impl IntoIterator<Item = u32, IntoIter = impl Iterator<Item = u32> + Send + Sync>
907        {
908            // Just a mock implementation so we just do the easy safe thing.
909            self.data[r].clone()
910        }
911    }
912
913    #[test]
914    fn test_dimensions() {
915        let dims = Dimensions {
916            width: 3,
917            height: 5,
918        };
919        assert_eq!(dims.width, 3);
920        assert_eq!(dims.height, 5);
921        assert_eq!(format!("{dims:?}"), "3x5");
922        assert_eq!(format!("{dims}"), "3x5");
923    }
924
925    #[test]
926    fn test_mock_matrix_dimensions() {
927        let matrix = MockMatrix {
928            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
929            width: 3,
930            height: 3,
931        };
932        assert_eq!(matrix.width(), 3);
933        assert_eq!(matrix.height(), 3);
934        assert_eq!(
935            matrix.dimensions(),
936            Dimensions {
937                width: 3,
938                height: 3
939            }
940        );
941    }
942
943    #[test]
944    fn test_first_row() {
945        let matrix = MockMatrix {
946            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
947            width: 3,
948            height: 3,
949        };
950        let mut first_row = matrix.first_row().unwrap().into_iter();
951        assert_eq!(first_row.next(), Some(1));
952        assert_eq!(first_row.next(), Some(2));
953        assert_eq!(first_row.next(), Some(3));
954    }
955
956    #[test]
957    fn test_last_row() {
958        let matrix = MockMatrix {
959            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
960            width: 3,
961            height: 3,
962        };
963        let mut last_row = matrix.last_row().unwrap().into_iter();
964        assert_eq!(last_row.next(), Some(7));
965        assert_eq!(last_row.next(), Some(8));
966        assert_eq!(last_row.next(), Some(9));
967    }
968
969    #[test]
970    fn test_first_last_row_empty_matrix() {
971        let matrix = MockMatrix {
972            data: vec![],
973            width: 3,
974            height: 0,
975        };
976        let first_row = matrix.first_row();
977        let last_row = matrix.last_row();
978        assert!(first_row.is_none());
979        assert!(last_row.is_none());
980    }
981
982    #[test]
983    fn test_to_row_major_matrix() {
984        let matrix = MockMatrix {
985            data: vec![vec![1, 2], vec![3, 4]],
986            width: 2,
987            height: 2,
988        };
989        let row_major = matrix.to_row_major_matrix();
990        assert_eq!(row_major.values, vec![1, 2, 3, 4]);
991        assert_eq!(row_major.width, 2);
992    }
993
994    #[test]
995    fn test_matrix_get_methods() {
996        let matrix = MockMatrix {
997            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
998            width: 3,
999            height: 3,
1000        };
1001        assert_eq!(matrix.get(0, 0), Some(1));
1002        assert_eq!(matrix.get(1, 2), Some(6));
1003        assert_eq!(matrix.get(2, 1), Some(8));
1004
1005        unsafe {
1006            assert_eq!(matrix.get_unchecked(0, 1), 2);
1007            assert_eq!(matrix.get_unchecked(1, 0), 4);
1008            assert_eq!(matrix.get_unchecked(2, 2), 9);
1009        }
1010
1011        assert_eq!(matrix.get(3, 0), None); // Height out of bounds
1012        assert_eq!(matrix.get(0, 3), None); // Width out of bounds
1013    }
1014
1015    #[test]
1016    fn test_matrix_row_methods_iteration() {
1017        let matrix = MockMatrix {
1018            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
1019            width: 3,
1020            height: 3,
1021        };
1022
1023        let mut row_iter = matrix.row(1).unwrap().into_iter();
1024        assert_eq!(row_iter.next(), Some(4));
1025        assert_eq!(row_iter.next(), Some(5));
1026        assert_eq!(row_iter.next(), Some(6));
1027        assert_eq!(row_iter.next(), None);
1028
1029        unsafe {
1030            let mut row_iter_unchecked = matrix.row_unchecked(2).into_iter();
1031            assert_eq!(row_iter_unchecked.next(), Some(7));
1032            assert_eq!(row_iter_unchecked.next(), Some(8));
1033            assert_eq!(row_iter_unchecked.next(), Some(9));
1034            assert_eq!(row_iter_unchecked.next(), None);
1035
1036            let mut row_iter_subset = matrix.row_subseq_unchecked(0, 1, 3).into_iter();
1037            assert_eq!(row_iter_subset.next(), Some(2));
1038            assert_eq!(row_iter_subset.next(), Some(3));
1039            assert_eq!(row_iter_subset.next(), None);
1040        }
1041
1042        assert!(matrix.row(3).is_none()); // Height out of bounds
1043    }
1044
1045    #[test]
1046    fn test_row_slice_methods() {
1047        let matrix = MockMatrix {
1048            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
1049            width: 3,
1050            height: 3,
1051        };
1052        let row_slice = matrix.row_slice(1).unwrap();
1053        assert_eq!(*row_slice, [4, 5, 6]);
1054        unsafe {
1055            let row_slice_unchecked = matrix.row_slice_unchecked(2);
1056            assert_eq!(*row_slice_unchecked, [7, 8, 9]);
1057
1058            let row_subslice = matrix.row_subslice_unchecked(0, 1, 2);
1059            assert_eq!(*row_subslice, [2]);
1060        }
1061
1062        assert!(matrix.row_slice(3).is_none()); // Height out of bounds
1063    }
1064
1065    #[test]
1066    fn test_matrix_rows() {
1067        let matrix = MockMatrix {
1068            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
1069            width: 3,
1070            height: 3,
1071        };
1072
1073        let all_rows: Vec<Vec<u32>> = matrix.rows().map(|row| row.collect()).collect();
1074        assert_eq!(all_rows, vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]]);
1075    }
1076
1077    #[test]
1078    fn test_rowwise_packed_dot_product() {
1079        use p3_field::PackedFieldExtension;
1080
1081        type F = BabyBear;
1082        type EF = BinomialExtensionField<BabyBear, 4>;
1083        type PF = <F as p3_field::Field>::Packing;
1084        type EFPacked = <EF as p3_field::ExtensionField<F>>::ExtensionPacking;
1085
1086        let mut rng = SmallRng::seed_from_u64(42);
1087
1088        // Test with various matrix dimensions to cover edge cases.
1089        for (height, width) in [(32, 16), (64, 128), (128, 17), (256, 255)] {
1090            let m = RowMajorMatrix::<F>::rand(&mut rng, height, width);
1091            let v = RowMajorMatrix::<EF>::rand(&mut rng, width, 1).values;
1092
1093            // Compute expected result naively: for each row, compute dot product with v.
1094            let expected: Vec<EF> = m
1095                .rows()
1096                .map(|row| {
1097                    row.into_iter()
1098                        .zip(v.iter())
1099                        .map(|(r, &ve)| ve * r)
1100                        .sum::<EF>()
1101                })
1102                .collect();
1103
1104            // Pack the vector for the optimized function.
1105            let packed_v: Vec<EFPacked> = v
1106                .chunks(<PF as PackedValue>::WIDTH)
1107                .map(|chunk| {
1108                    let mut padded = EF::zero_vec(<PF as PackedValue>::WIDTH);
1109                    padded[..chunk.len()].copy_from_slice(chunk);
1110                    EFPacked::from_ext_slice(&padded)
1111                })
1112                .collect();
1113
1114            // Compute using the optimized function.
1115            let result: Vec<EF> = m.rowwise_packed_dot_product::<EF>(&packed_v).collect();
1116
1117            assert_eq!(result, expected, "Mismatch for matrix {}x{}", height, width);
1118        }
1119    }
1120}