Skip to main content

nuvec_cpu/
matmul.rs

1//! Matrix multiplication over strided operands, backed by the pure-Rust `gemm` crate.
2
3use half::f16;
4
5/// A strided matrix: element `(i, j)` lives at `data[offset + i·row_stride + j·col_stride]`.
6#[derive(Debug, Clone, Copy)]
7pub struct MatView<'a, T> {
8    /// The underlying storage.
9    pub data: &'a [T],
10    /// Position of element `(0, 0)`, in elements.
11    pub offset: isize,
12    /// Step between rows, in elements. May be zero or negative.
13    pub row_stride: isize,
14    /// Step between columns, in elements. May be zero or negative.
15    pub col_stride: isize,
16}
17
18impl<T> MatView<'_, T> {
19    /// Asserts that every element of a `rows × cols` matrix lies inside `data`.
20    fn assert_in_bounds(&self, rows: usize, cols: usize) {
21        if rows == 0 || cols == 0 {
22            return;
23        }
24        let span = |n: usize, stride: isize| (n as isize - 1) * stride;
25        let (r, c) = (span(rows, self.row_stride), span(cols, self.col_stride));
26        let lo = self.offset + r.min(0) + c.min(0);
27        let hi = self.offset + r.max(0) + c.max(0);
28        assert!(
29            lo >= 0 && (hi as usize) < self.data.len(),
30            "matrix view reaches outside its data"
31        );
32    }
33}
34
35mod sealed {
36    pub trait Sealed {}
37}
38
39/// An element type with a GEMM kernel: `f32`, `f64`, `f16`, and `[f32; 2]` (complex64 as
40/// `(re, im)`).
41pub trait Gemm: Copy + Default + sealed::Sealed + 'static {
42    /// `acc + a·b`, used by the reference loop (and under Miri).
43    fn mul_add(acc: Self, a: Self, b: Self) -> Self;
44
45    /// Calls `gemm::gemm` for `dst (row-major m×n) = lhs · rhs`.
46    ///
47    /// # Safety
48    ///
49    /// Every element reachable through the views must lie inside their data, and `dst` must
50    /// hold `m·n` elements.
51    #[cfg(not(miri))]
52    unsafe fn gemm(
53        m: usize,
54        n: usize,
55        k: usize,
56        dst: &mut [Self],
57        a: MatView<'_, Self>,
58        b: MatView<'_, Self>,
59    );
60}
61
62macro_rules! impl_gemm {
63    ($t:ty, $g:ty, $mul_add:expr, $one:expr) => {
64        impl sealed::Sealed for $t {}
65        impl Gemm for $t {
66            fn mul_add(acc: Self, a: Self, b: Self) -> Self {
67                let f: fn(Self, Self, Self) -> Self = $mul_add;
68                f(acc, a, b)
69            }
70
71            #[cfg(not(miri))]
72            unsafe fn gemm(
73                m: usize,
74                n: usize,
75                k: usize,
76                dst: &mut [Self],
77                a: MatView<'_, Self>,
78                b: MatView<'_, Self>,
79            ) {
80                let lhs = a.data.as_ptr().wrapping_offset(a.offset).cast::<$g>();
81                let rhs = b.data.as_ptr().wrapping_offset(b.offset).cast::<$g>();
82                // SAFETY: the caller guarantees that every element reachable through `a`
83                // and `b` is in bounds and that `dst` holds `m·n` elements, written with
84                // row stride `n` and column stride 1. `$t` and `$g` have the same size and
85                // layout. `read_dst = false`, so `dst` is only written.
86                unsafe {
87                    gemm::gemm::<$g>(
88                        m,
89                        n,
90                        k,
91                        dst.as_mut_ptr().cast::<$g>(),
92                        1,
93                        n as isize,
94                        false,
95                        lhs,
96                        a.col_stride,
97                        a.row_stride,
98                        rhs,
99                        b.col_stride,
100                        b.row_stride,
101                        $one,
102                        $one,
103                        false,
104                        false,
105                        false,
106                        gemm::Parallelism::Rayon(0),
107                    );
108                }
109            }
110        }
111    };
112}
113
114impl_gemm!(f32, f32, |acc, a, b| acc + a * b, 1.0);
115impl_gemm!(f64, f64, |acc, a, b| acc + a * b, 1.0);
116impl_gemm!(
117    f16,
118    gemm::f16,
119    |acc, a, b| f16::from_f32(acc.to_f32() + a.to_f32() * b.to_f32()),
120    gemm::f16::ONE
121);
122impl_gemm!(
123    [f32; 2],
124    gemm::c32,
125    |acc, a, b| [
126        acc[0] + a[0] * b[0] - a[1] * b[1],
127        acc[1] + a[0] * b[1] + a[1] * b[0]
128    ],
129    gemm::c32::new(1.0, 0.0)
130);
131
132/// `out = a · b` for an `m × k` matrix `a` and a `k × n` matrix `b`; `out` is row-major
133/// `m × n`. Operands may have any strides (transposed, reversed, broadcast).
134///
135/// # Panics
136///
137/// If `out.len() != m·n` or a view reaches outside its data.
138pub fn matmul<T: Gemm>(
139    m: usize,
140    n: usize,
141    k: usize,
142    a: MatView<'_, T>,
143    b: MatView<'_, T>,
144    out: &mut [T],
145) {
146    assert_eq!(out.len(), m * n, "output length does not match m·n");
147    a.assert_in_bounds(m, k);
148    b.assert_in_bounds(k, n);
149    if m == 0 || n == 0 {
150        return;
151    }
152    if k == 0 {
153        out.fill(T::default());
154        return;
155    }
156    #[cfg(not(miri))]
157    // SAFETY: both views were checked to be in bounds for their extents above, and `out`
158    // holds exactly `m·n` elements.
159    unsafe {
160        T::gemm(m, n, k, out, a, b);
161    }
162    #[cfg(miri)]
163    naive(m, n, k, a, b, out);
164}
165
166/// The textbook triple loop: the reference for tests, and the kernel under Miri (which
167/// cannot execute SIMD intrinsics).
168#[cfg_attr(not(any(miri, test)), allow(dead_code))]
169fn naive<T: Gemm>(
170    m: usize,
171    n: usize,
172    k: usize,
173    a: MatView<'_, T>,
174    b: MatView<'_, T>,
175    out: &mut [T],
176) {
177    let at = |v: &MatView<'_, T>, i: usize, j: usize| {
178        v.data[(v.offset + i as isize * v.row_stride + j as isize * v.col_stride) as usize]
179    };
180    for i in 0..m {
181        for j in 0..n {
182            out[i * n + j] = (0..k).fold(T::default(), |acc, p| {
183                T::mul_add(acc, at(&a, i, p), at(&b, p, j))
184            });
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn matches_naive_on_strided_operands() {
195        let (m, n, k) = (7, 5, 9);
196        let data: Vec<f64> = (0..200).map(|i| (i as f64 * 0.37).sin()).collect();
197        // `a` read transposed from the middle, `b` with a negative row stride.
198        let a = MatView {
199            data: &data,
200            offset: 10,
201            row_stride: 1,
202            col_stride: 11,
203        };
204        let b = MatView {
205            data: &data,
206            offset: 150,
207            row_stride: -12,
208            col_stride: 2,
209        };
210        let (mut fast, mut slow) = (vec![0.0; m * n], vec![0.0; m * n]);
211        matmul(m, n, k, a, b, &mut fast);
212        naive(m, n, k, a, b, &mut slow);
213        for (x, y) in fast.iter().zip(&slow) {
214            assert!((x - y).abs() < 1e-12, "{x} vs {y}");
215        }
216    }
217
218    #[test]
219    fn complex_and_zero_k() {
220        let a = [[1.0f32, 2.0], [0.0, 1.0]]; // 1x2: [1+2i, i]
221        let b = [[3.0f32, 0.0], [0.0, -1.0]]; // 2x1: [3, -i]
222        let mut out = [[0.0f32; 2]; 1];
223        let av = MatView {
224            data: &a,
225            offset: 0,
226            row_stride: 2,
227            col_stride: 1,
228        };
229        let bv = MatView {
230            data: &b,
231            offset: 0,
232            row_stride: 1,
233            col_stride: 1,
234        };
235        matmul(1, 1, 2, av, bv, &mut out);
236        assert_eq!(out, [[4.0, 6.0]]); // (1+2i)·3 + i·(-i) = 4 + 6i
237        let mut zeros = [9.0f32; 4];
238        let empty = MatView {
239            data: &[0.0f32; 0],
240            offset: 0,
241            row_stride: 0,
242            col_stride: 0,
243        };
244        matmul(2, 2, 0, empty, empty, &mut zeros);
245        assert_eq!(zeros, [0.0; 4]);
246    }
247
248    #[test]
249    #[should_panic(expected = "outside its data")]
250    fn rejects_out_of_bounds_views() {
251        let data = [0.0f32; 4];
252        let v = MatView {
253            data: &data,
254            offset: 0,
255            row_stride: 2,
256            col_stride: 1,
257        };
258        let mut out = [0.0f32; 9];
259        matmul(3, 3, 2, v, v, &mut out);
260    }
261}