1use std::fmt;
4
5use half::f16;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum MatmulError {
11 Overflow,
13 OutputLength,
15 OutOfBounds,
17}
18
19impl fmt::Display for MatmulError {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 f.write_str(match self {
22 Self::Overflow => "matmul: sizes overflow",
23 Self::OutputLength => "matmul: output length does not match m·n",
24 Self::OutOfBounds => "matmul: matrix view reaches outside its data",
25 })
26 }
27}
28
29impl std::error::Error for MatmulError {}
30
31#[derive(Debug, Clone, Copy)]
33pub struct MatView<'a, T> {
34 pub data: &'a [T],
36 pub offset: isize,
38 pub row_stride: isize,
40 pub col_stride: isize,
42}
43
44impl<T> MatView<'_, T> {
45 fn check_in_bounds(&self, rows: usize, cols: usize) -> Result<(), MatmulError> {
48 if rows == 0 || cols == 0 {
49 return Ok(());
50 }
51 let span = |n: usize, stride: isize| {
52 isize::try_from(n - 1)
53 .ok()
54 .and_then(|n| n.checked_mul(stride))
55 .ok_or(MatmulError::Overflow)
56 };
57 let (r, c) = (span(rows, self.row_stride)?, span(cols, self.col_stride)?);
58 let reach = |a: isize, b: isize| {
59 self.offset
60 .checked_add(a)
61 .and_then(|x| x.checked_add(b))
62 .ok_or(MatmulError::Overflow)
63 };
64 let lo = reach(r.min(0), c.min(0))?;
65 let hi = reach(r.max(0), c.max(0))?;
66 if lo >= 0 && (hi as usize) < self.data.len() {
67 Ok(())
68 } else {
69 Err(MatmulError::OutOfBounds)
70 }
71 }
72}
73
74mod sealed {
75 pub trait Sealed {}
76}
77
78pub trait Gemm: Copy + Default + sealed::Sealed + 'static {
81 fn mul_add(acc: Self, a: Self, b: Self) -> Self;
83
84 #[cfg(not(miri))]
91 unsafe fn gemm(
92 m: usize,
93 n: usize,
94 k: usize,
95 dst: &mut [Self],
96 a: MatView<'_, Self>,
97 b: MatView<'_, Self>,
98 );
99}
100
101macro_rules! impl_gemm {
102 ($t:ty, $g:ty, $mul_add:expr, $one:expr) => {
103 impl sealed::Sealed for $t {}
104 impl Gemm for $t {
105 fn mul_add(acc: Self, a: Self, b: Self) -> Self {
106 let f: fn(Self, Self, Self) -> Self = $mul_add;
107 f(acc, a, b)
108 }
109
110 #[cfg(not(miri))]
111 unsafe fn gemm(
112 m: usize,
113 n: usize,
114 k: usize,
115 dst: &mut [Self],
116 a: MatView<'_, Self>,
117 b: MatView<'_, Self>,
118 ) {
119 let lhs = a.data.as_ptr().wrapping_offset(a.offset).cast::<$g>();
120 let rhs = b.data.as_ptr().wrapping_offset(b.offset).cast::<$g>();
121 unsafe {
126 gemm::gemm::<$g>(
127 m,
128 n,
129 k,
130 dst.as_mut_ptr().cast::<$g>(),
131 1,
132 n as isize,
133 false,
134 lhs,
135 a.col_stride,
136 a.row_stride,
137 rhs,
138 b.col_stride,
139 b.row_stride,
140 $one,
141 $one,
142 false,
143 false,
144 false,
145 gemm::Parallelism::Rayon(0),
146 );
147 }
148 }
149 }
150 };
151}
152
153impl_gemm!(f32, f32, |acc, a, b| acc + a * b, 1.0);
154impl_gemm!(f64, f64, |acc, a, b| acc + a * b, 1.0);
155
156impl sealed::Sealed for f16 {}
157impl Gemm for f16 {
158 fn mul_add(acc: Self, a: Self, b: Self) -> Self {
159 f16::from_f32(acc.to_f32() + a.to_f32() * b.to_f32())
160 }
161
162 #[cfg(not(miri))]
165 unsafe fn gemm(
166 m: usize,
167 n: usize,
168 k: usize,
169 dst: &mut [Self],
170 a: MatView<'_, Self>,
171 b: MatView<'_, Self>,
172 ) {
173 let widen = |v: MatView<'_, Self>, rows: usize, cols: usize| -> Vec<f32> {
174 (0..rows)
175 .flat_map(|i| {
176 (0..cols).map(move |j| {
177 let at = v.offset + i as isize * v.row_stride + j as isize * v.col_stride;
178 v.data[at as usize].to_f32()
179 })
180 })
181 .collect()
182 };
183 let (a32, b32) = (widen(a, m, k), widen(b, k, n));
184 let view = |data, cols: usize| MatView {
185 data,
186 offset: 0,
187 row_stride: cols as isize,
188 col_stride: 1,
189 };
190 let mut out = vec![0.0f32; m * n];
191 unsafe { f32::gemm(m, n, k, &mut out, view(&a32, k), view(&b32, n)) };
194 for (d, v) in dst.iter_mut().zip(out) {
195 *d = f16::from_f32(v);
196 }
197 }
198}
199impl_gemm!(
200 [f32; 2],
201 gemm::c32,
202 |acc, a, b| [
203 acc[0] + a[0] * b[0] - a[1] * b[1],
204 acc[1] + a[0] * b[1] + a[1] * b[0]
205 ],
206 gemm::c32::new(1.0, 0.0)
207);
208
209pub fn matmul<T: Gemm>(
217 m: usize,
218 n: usize,
219 k: usize,
220 a: MatView<'_, T>,
221 b: MatView<'_, T>,
222 out: &mut [T],
223) -> Result<(), MatmulError> {
224 let size = m.checked_mul(n).ok_or(MatmulError::Overflow)?;
225 if out.len() != size {
226 return Err(MatmulError::OutputLength);
227 }
228 a.check_in_bounds(m, k)?;
229 b.check_in_bounds(k, n)?;
230 if m == 0 || n == 0 {
231 return Ok(());
232 }
233 if k == 0 {
234 out.fill(T::default());
235 return Ok(());
236 }
237 #[cfg(not(miri))]
238 unsafe {
241 T::gemm(m, n, k, out, a, b);
242 }
243 #[cfg(miri)]
244 naive(m, n, k, a, b, out);
245 Ok(())
246}
247
248#[cfg_attr(not(any(miri, test)), allow(dead_code))]
251fn naive<T: Gemm>(
252 m: usize,
253 n: usize,
254 k: usize,
255 a: MatView<'_, T>,
256 b: MatView<'_, T>,
257 out: &mut [T],
258) {
259 let at = |v: &MatView<'_, T>, i: usize, j: usize| {
260 v.data[(v.offset + i as isize * v.row_stride + j as isize * v.col_stride) as usize]
261 };
262 for i in 0..m {
263 for j in 0..n {
264 out[i * n + j] = (0..k).fold(T::default(), |acc, p| {
265 T::mul_add(acc, at(&a, i, p), at(&b, p, j))
266 });
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn matches_naive_on_strided_operands() {
277 let (m, n, k) = (7, 5, 9);
278 let data: Vec<f64> = (0..200).map(|i| (i as f64 * 0.37).sin()).collect();
279 let a = MatView {
281 data: &data,
282 offset: 10,
283 row_stride: 1,
284 col_stride: 11,
285 };
286 let b = MatView {
287 data: &data,
288 offset: 150,
289 row_stride: -12,
290 col_stride: 2,
291 };
292 let (mut fast, mut slow) = (vec![0.0; m * n], vec![0.0; m * n]);
293 matmul(m, n, k, a, b, &mut fast).unwrap();
294 naive(m, n, k, a, b, &mut slow);
295 for (x, y) in fast.iter().zip(&slow) {
296 assert!((x - y).abs() < 1e-12, "{x} vs {y}");
297 }
298 }
299
300 #[test]
301 fn complex_and_zero_k() {
302 let a = [[1.0f32, 2.0], [0.0, 1.0]]; let b = [[3.0f32, 0.0], [0.0, -1.0]]; let mut out = [[0.0f32; 2]; 1];
305 let av = MatView {
306 data: &a,
307 offset: 0,
308 row_stride: 2,
309 col_stride: 1,
310 };
311 let bv = MatView {
312 data: &b,
313 offset: 0,
314 row_stride: 1,
315 col_stride: 1,
316 };
317 matmul(1, 1, 2, av, bv, &mut out).unwrap();
318 assert_eq!(out, [[4.0, 6.0]]); let mut zeros = [9.0f32; 4];
320 let empty = MatView {
321 data: &[0.0f32; 0],
322 offset: 0,
323 row_stride: 0,
324 col_stride: 0,
325 };
326 matmul(2, 2, 0, empty, empty, &mut zeros).unwrap();
327 assert_eq!(zeros, [0.0; 4]);
328 }
329
330 #[test]
331 fn rejects_out_of_bounds_views() {
332 let data = [0.0f32; 4];
333 let v = MatView {
334 data: &data,
335 offset: 0,
336 row_stride: 2,
337 col_stride: 1,
338 };
339 let mut out = [0.0f32; 9];
340 assert_eq!(
341 matmul(3, 3, 2, v, v, &mut out),
342 Err(MatmulError::OutOfBounds)
343 );
344 }
345
346 #[test]
347 fn overflowing_sizes_and_wrong_outputs_are_errors() {
348 let data = [1.0f32];
351 let v = MatView {
352 data: &data,
353 offset: 0,
354 row_stride: 0,
355 col_stride: 0,
356 };
357 let mut out = [7.0f32; 0];
358 assert_eq!(
359 matmul(1 << 33, 1 << 31, 1, v, v, &mut out),
360 Err(MatmulError::Overflow)
361 );
362 let mut out = [7.0f32; 5];
363 assert_eq!(
364 matmul(2, 3, 1, v, v, &mut out),
365 Err(MatmulError::OutputLength)
366 );
367 assert_eq!(out, [7.0; 5], "written before the check");
368 let far = MatView {
370 data: &data,
371 offset: 0,
372 row_stride: isize::MAX,
373 col_stride: 0,
374 };
375 let mut out = [0.0f32; 3];
376 assert_eq!(
377 matmul(3, 1, 1, far, v, &mut out),
378 Err(MatmulError::Overflow)
379 );
380 let negative = MatView {
381 data: &data,
382 offset: 0,
383 row_stride: isize::MIN,
384 col_stride: 0,
385 };
386 assert_eq!(
387 matmul(3, 1, 1, negative, v, &mut out),
388 Err(MatmulError::Overflow)
389 );
390 }
391
392 #[test]
393 #[cfg_attr(miri, ignore = "half's f16 conversions use inline assembly")]
394 fn f16_accumulates_like_f32() {
395 let (m, n, k) = (4, 3, 4096);
398 let a: Vec<f16> = (0..m * k)
399 .map(|i| f16::from_f32(((i as f32) * 0.618).sin()))
400 .collect();
401 let b: Vec<f16> = (0..k * n)
402 .map(|i| f16::from_f32(((i as f32) * 0.377).cos()))
403 .collect();
404 let av = MatView {
405 data: &a,
406 offset: 0,
407 row_stride: k as isize,
408 col_stride: 1,
409 };
410 let bv = MatView {
411 data: &b,
412 offset: 0,
413 row_stride: n as isize,
414 col_stride: 1,
415 };
416 let mut out = vec![f16::ZERO; m * n];
417 matmul(m, n, k, av, bv, &mut out).unwrap();
418 for i in 0..m {
419 for j in 0..n {
420 let want: f32 = (0..k)
421 .map(|p| a[i * k + p].to_f32() * b[p * n + j].to_f32())
422 .sum();
423 let got = out[i * n + j].to_f32();
424 let tol = want.abs() * 2.0f32.powi(-10) + 1e-3;
426 assert!((got - want).abs() <= tol, "({i}, {j}): {got} vs {want}");
427 }
428 }
429 }
430}