Skip to main content

kime_cpu/
gemm.rs

1//! FP32 matrix products in the layout of a PyTorch `Linear`: `y = x wᵀ + b`, with `x` as `[m, k]`
2//! and `w` as `[n, k]`, both row major.
3//!
4//! The weights are packed once into panels of 16 rows, `[n / 16][k][16]` with the last panel
5//! padded with zeros, and a micro kernel keeps a 6 by 16 block of outputs in registers while it
6//! walks `k`, one broadcast of `x` and two vector loads of the panel per step. Every output is
7//! summed the same way wherever it lands: in order over `k` with fused multiply adds in f32,
8//! moved to an f64 sum every 64 steps, then rounded to f32 and given its bias. Tiling and threading
9//! only decide which outputs run side by side, so the result is the same bit for bit for any
10//! thread count and any split, and the same on x86 with FMA as on ARM.
11//!
12//! On macOS the GEMM goes to Accelerate instead, whose `sgemm` runs on the AMX units at two to
13//! four times what the NEON kernel reaches. Its sums are in an order of its own that changes with
14//! the number of rows in the call, so it is always called on blocks of exactly 64 rows, the last
15//! one padded with zeros. With the row count fixed, a row's result does not depend on the rows
16//! around it or on its place in the block, so the bits still do not depend on the batch or the
17//! split. They are not the same bits as on other machines, which the parity tests allow for.
18//!
19//! [`dot`], which attention uses for its scores, sums in eight lanes instead and is not meant to
20//! match the GEMM bit for bit.
21
22use kime_tensor::Epilogue;
23
24use crate::ops::gelu;
25use crate::par::{self, Shared};
26
27const LANES: usize = 8;
28/// Elements of `k` summed in f32 before the running sums move to f64.
29const BLOCK: usize = 64;
30/// Rows of `x` per micro tile.
31const MR: usize = 6;
32/// Rows of `w` per panel, two vectors.
33pub const NR: usize = 16;
34/// Rows of `w` per task, a multiple of NR sized so a task's panels stay in L2.
35#[cfg(not(target_os = "macos"))]
36const NB: usize = 4 * NR;
37/// Rows of `x` per task, a multiple of MR.
38#[cfg(not(target_os = "macos"))]
39const MB: usize = 24 * MR;
40
41/// `dot(a, b)` in the order described in the module docs.
42///
43/// # Panics
44///
45/// If the lengths differ.
46#[must_use]
47pub fn dot(a: &[f32], b: &[f32]) -> f32 {
48    assert_eq!(a.len(), b.len());
49    #[cfg(target_arch = "aarch64")]
50    return dot_v::<neon::Neon>(a, b);
51    #[cfg(target_arch = "x86_64")]
52    if has_fma() {
53        // SAFETY: the features dot_fma enables were detected on this machine.
54        return unsafe { dot_fma(a, b) };
55    }
56    #[allow(unreachable_code)]
57    dot_v::<[f32; LANES]>(a, b)
58}
59
60#[cfg(target_arch = "x86_64")]
61#[target_feature(enable = "avx2,fma")]
62fn dot_fma(a: &[f32], b: &[f32]) -> f32 {
63    dot_v::<avx::Avx>(a, b)
64}
65
66#[cfg(target_arch = "x86_64")]
67#[inline]
68fn has_fma() -> bool {
69    std::arch::is_x86_feature_detected!("avx2") && std::arch::is_x86_feature_detected!("fma")
70}
71
72/// Eight f32 lanes with a fused multiply add, the one vector op the kernels need. Each lane is
73/// its own running sum, so every implementation gives the same bits.
74trait V8: Copy {
75    fn zero() -> Self;
76    fn splat(v: f32) -> Self;
77    fn load(s: &[f32; LANES]) -> Self;
78    /// `self + a * b`, rounded once.
79    fn fma(self, a: Self, b: Self) -> Self;
80    fn lanes(self) -> [f32; LANES];
81}
82
83impl V8 for [f32; LANES] {
84    #[inline(always)]
85    fn zero() -> Self {
86        [0.0; LANES]
87    }
88    #[inline(always)]
89    fn splat(v: f32) -> Self {
90        [v; LANES]
91    }
92    #[inline(always)]
93    fn load(s: &[f32; LANES]) -> Self {
94        *s
95    }
96    #[inline(always)]
97    fn fma(self, a: Self, b: Self) -> Self {
98        std::array::from_fn(|l| a[l].mul_add(b[l], self[l]))
99    }
100    #[inline(always)]
101    fn lanes(self) -> [f32; LANES] {
102        self
103    }
104}
105
106#[cfg(target_arch = "aarch64")]
107mod neon {
108    use std::arch::aarch64::{float32x4_t, vdupq_n_f32, vfmaq_f32, vld1q_f32, vst1q_f32};
109
110    use super::{LANES, V8};
111
112    #[derive(Clone, Copy)]
113    pub(super) struct Neon(float32x4_t, float32x4_t);
114
115    impl V8 for Neon {
116        #[inline(always)]
117        fn zero() -> Self {
118            // SAFETY: NEON is part of the aarch64 baseline.
119            unsafe { Self(vdupq_n_f32(0.0), vdupq_n_f32(0.0)) }
120        }
121        #[inline(always)]
122        fn splat(v: f32) -> Self {
123            // SAFETY: NEON is part of the aarch64 baseline.
124            unsafe { Self(vdupq_n_f32(v), vdupq_n_f32(v)) }
125        }
126        #[inline(always)]
127        fn load(s: &[f32; LANES]) -> Self {
128            // SAFETY: both loads read four floats inside the eight the reference covers.
129            unsafe { Self(vld1q_f32(s.as_ptr()), vld1q_f32(s.as_ptr().add(4))) }
130        }
131        #[inline(always)]
132        fn fma(self, a: Self, b: Self) -> Self {
133            // SAFETY: NEON is part of the aarch64 baseline.
134            unsafe { Self(vfmaq_f32(self.0, a.0, b.0), vfmaq_f32(self.1, a.1, b.1)) }
135        }
136        #[inline(always)]
137        fn lanes(self) -> [f32; LANES] {
138            let mut out = [0f32; LANES];
139            // SAFETY: both stores write four floats inside the eight of out.
140            unsafe {
141                vst1q_f32(out.as_mut_ptr(), self.0);
142                vst1q_f32(out.as_mut_ptr().add(4), self.1);
143            }
144            out
145        }
146    }
147}
148
149#[cfg(target_arch = "x86_64")]
150mod avx {
151    use std::arch::x86_64::{
152        __m256, _mm256_fmadd_ps, _mm256_loadu_ps, _mm256_set1_ps, _mm256_setzero_ps,
153        _mm256_storeu_ps,
154    };
155
156    use super::{LANES, V8};
157
158    /// Only built inside functions that enable avx2 and fma, after detecting them.
159    #[derive(Clone, Copy)]
160    pub(super) struct Avx(__m256);
161
162    impl V8 for Avx {
163        #[inline(always)]
164        fn zero() -> Self {
165            // SAFETY: Avx values only exist on machines where AVX was detected.
166            unsafe { Self(_mm256_setzero_ps()) }
167        }
168        #[inline(always)]
169        fn splat(v: f32) -> Self {
170            // SAFETY: Avx values only exist on machines where AVX was detected.
171            unsafe { Self(_mm256_set1_ps(v)) }
172        }
173        #[inline(always)]
174        fn load(s: &[f32; LANES]) -> Self {
175            // SAFETY: an unaligned load of the eight floats of s, on a machine with AVX.
176            unsafe { Self(_mm256_loadu_ps(s.as_ptr())) }
177        }
178        #[inline(always)]
179        fn fma(self, a: Self, b: Self) -> Self {
180            // SAFETY: FMA was detected before any Avx value was made.
181            unsafe { Self(_mm256_fmadd_ps(a.0, b.0, self.0)) }
182        }
183        #[inline(always)]
184        fn lanes(self) -> [f32; LANES] {
185            let mut out = [0f32; LANES];
186            // SAFETY: an unaligned store of eight floats into out, on a machine with AVX.
187            unsafe { _mm256_storeu_ps(out.as_mut_ptr(), self.0) };
188            out
189        }
190    }
191}
192
193/// `a · b` in eight lanes: running sums over `k` in steps of eight with fused multiply adds, moved
194/// to f64 every 64 elements, the eight added in a fixed tree, then the leftover `k % 8` terms in
195/// order.
196#[inline(always)]
197fn dot_v<V: V8>(a: &[f32], b: &[f32]) -> f32 {
198    let k = a.len();
199    let body = k - k % LANES;
200    let mut wide = [0f64; LANES];
201    let mut p = 0;
202    while p < body {
203        let end = (p + BLOCK).min(body);
204        let mut acc = V::zero();
205        while p < end {
206            acc = acc.fma(
207                V::load(a[p..p + LANES].try_into().unwrap()),
208                V::load(b[p..p + LANES].try_into().unwrap()),
209            );
210            p += LANES;
211        }
212        for (w, l) in wide.iter_mut().zip(acc.lanes()) {
213            *w += f64::from(l);
214        }
215    }
216    let v = wide;
217    let mut s = ((v[0] + v[4]) + (v[2] + v[6])) + ((v[1] + v[5]) + (v[3] + v[7]));
218    for q in body..k {
219        s = f64::from(a[q]).mul_add(f64::from(b[q]), s);
220    }
221    s as f32
222}
223
224/// Lays out `w`, `[n, k]` row major, the way [`Gemm`] reads it: panels of 16 rows, or as it is
225/// on macOS, where Accelerate reads it.
226///
227/// # Panics
228///
229/// If `w` is not `[n, k]`.
230#[must_use]
231pub fn pack(w: &[f32], n: usize, k: usize) -> Vec<f32> {
232    assert_eq!(w.len(), n * k, "w is not [n, k]");
233    if cfg!(target_os = "macos") { w.to_vec() } else { pack_panels(w, n, k) }
234}
235
236/// Values in `w` as [`pack`] lays it out.
237fn packed_len(n: usize, k: usize) -> usize {
238    if cfg!(target_os = "macos") { n * k } else { n.div_ceil(NR) * NR * k }
239}
240
241/// Floats of scratch each task of a GEMM with these sizes needs.
242#[must_use]
243pub fn scratch_len(k: usize, n: usize) -> usize {
244    #[cfg(target_os = "macos")]
245    return blas::ROWS * (k + 3 * n.min(blas::COLS)) + 1;
246    #[cfg(not(target_os = "macos"))]
247    {
248        let _ = (k, n);
249        0
250    }
251}
252
253/// `w` in panels: `[n.div_ceil(16)][k][16]`, with the rows past `n` zero.
254#[cfg_attr(target_os = "macos", allow(dead_code))]
255fn pack_panels(w: &[f32], n: usize, k: usize) -> Vec<f32> {
256    let panels = n.div_ceil(NR);
257    let mut out = vec![0f32; panels * k * NR];
258    if k == 0 {
259        return out;
260    }
261    for (p, panel) in out.chunks_exact_mut(k * NR).enumerate() {
262        for c in 0..NR.min(n - p * NR) {
263            let row = &w[(p * NR + c) * k..][..k];
264            for (q, &v) in row.iter().enumerate() {
265                panel[q * NR + c] = v;
266            }
267        }
268    }
269    out
270}
271
272/// Rows `i..i + R` of `x` against one panel, the sums before the bias.
273///
274/// # Safety
275///
276/// Rows `i..i + R` must be in `x`, which is `[_, k]`, and `panel` must hold `k * NR` values.
277#[inline(always)]
278#[cfg_attr(target_os = "macos", allow(dead_code))]
279unsafe fn kernel<V: V8, const R: usize>(
280    x: &[f32],
281    k: usize,
282    i: usize,
283    panel: &[f32],
284) -> [[f32; NR]; R] {
285    let xs: [*const f32; R] = std::array::from_fn(|r| x.as_ptr().wrapping_add((i + r) * k));
286    let pw = panel.as_ptr();
287    let mut wide = [[0f64; NR]; R];
288    let mut q = 0;
289    while q < k {
290        let end = (q + BLOCK).min(k);
291        let mut acc = [[V::zero(); 2]; R];
292        while q < end {
293            // SAFETY: q < k, so the 16 values of step q are in the panel and x[i + r][q] in x.
294            let (w0, w1) = unsafe {
295                let at = pw.add(q * NR);
296                (
297                    V::load(&*at.cast::<[f32; LANES]>()),
298                    V::load(&*at.add(LANES).cast::<[f32; LANES]>()),
299                )
300            };
301            for r in 0..R {
302                // SAFETY: as above.
303                let xv = V::splat(unsafe { *xs[r].add(q) });
304                acc[r][0] = acc[r][0].fma(xv, w0);
305                acc[r][1] = acc[r][1].fma(xv, w1);
306            }
307            q += 1;
308        }
309        for r in 0..R {
310            for h in 0..2 {
311                for (w, l) in wide[r][h * LANES..][..LANES].iter_mut().zip(acc[r][h].lanes()) {
312                    *w += f64::from(l);
313                }
314            }
315        }
316    }
317    wide.map(|row| row.map(|v| v as f32))
318}
319
320#[cfg_attr(target_os = "macos", allow(dead_code))]
321struct Args<'a> {
322    x: &'a [f32],
323    w: &'a [f32],
324    b: Option<&'a [f32]>,
325    ep: Epilogue,
326    k: usize,
327    n: usize,
328    y: &'a Shared<'a>,
329}
330
331#[cfg_attr(target_os = "macos", allow(dead_code))]
332impl Args<'_> {
333    #[inline(always)]
334    fn run<V: V8, const R: usize>(&self, i: usize, p: usize) {
335        let k = self.k;
336        let panel = &self.w[p * k * NR..][..k * NR];
337        // SAFETY: the caller keeps i + R within m, and the panel was sliced to k * NR.
338        let out = unsafe { kernel::<V, R>(self.x, k, i, panel) };
339        let cols = NR.min(self.n - p * NR);
340        for (r, row) in out.iter().enumerate() {
341            for (c, &v) in row[..cols].iter().enumerate() {
342                self.put(i + r, p * NR + c, v);
343            }
344        }
345    }
346
347    /// Adds the bias, applies the epilogue and stores element `(i, j)`.
348    #[inline(always)]
349    fn put(&self, i: usize, j: usize, v: f32) {
350        let v = match self.b {
351            Some(b) => v + b[j],
352            None => v,
353        };
354        let at = i * self.n + j;
355        let v = match self.ep {
356            Epilogue::None => v,
357            Epilogue::Gelu => gelu(v),
358            Epilogue::Relu => v.max(0.0),
359            // SAFETY: each (i, j) pair belongs to exactly one task and one tile within it.
360            Epilogue::Accumulate => v + unsafe { self.y.get(at) },
361        };
362        // SAFETY: as above.
363        unsafe { self.y.set(at, v) };
364    }
365
366    /// Finishes columns `j0..` of row `i` of `y` from their sums, as [`put`](Self::put) does for one element, with
367    /// the choices made once for the row.
368    #[cfg(target_os = "macos")]
369    fn put_row(&self, i: usize, j0: usize, sums: &[f32]) {
370        // SAFETY: each stretch of a row belongs to exactly one task.
371        let y = unsafe { self.y.slice_mut(i * self.n + j0, sums.len()) };
372        let biased = |j: usize, v: f32| match self.b {
373            Some(b) => v + b[j0 + j],
374            None => v,
375        };
376        match (self.b, self.ep) {
377            (None, Epilogue::None) => y.copy_from_slice(sums),
378            (_, Epilogue::None) => {
379                y.iter_mut().zip(sums).enumerate().for_each(|(j, (y, &v))| *y = biased(j, v))
380            }
381            (_, Epilogue::Gelu) => {
382                y.iter_mut().zip(sums).enumerate().for_each(|(j, (y, &v))| *y = gelu(biased(j, v)))
383            }
384            (_, Epilogue::Relu) => y
385                .iter_mut()
386                .zip(sums)
387                .enumerate()
388                .for_each(|(j, (y, &v))| *y = biased(j, v).max(0.0)),
389            (Some(b), Epilogue::Accumulate) => {
390                y.iter_mut().zip(sums).zip(&b[j0..]).for_each(|((y, &v), &b)| *y += v + b)
391            }
392            (None, Epilogue::Accumulate) => y.iter_mut().zip(sums).for_each(|(y, &v)| *y += v),
393        }
394    }
395
396    /// Rows `rows` against the panels `panels`.
397    #[inline(always)]
398    fn block<V: V8>(&self, rows: (usize, usize), panels: (usize, usize)) {
399        for p in panels.0..panels.1 {
400            let mut i = rows.0;
401            while i + MR <= rows.1 {
402                self.run::<V, MR>(i, p);
403                i += MR;
404            }
405            match rows.1 - i {
406                0 => {}
407                1 => self.run::<V, 1>(i, p),
408                2 => self.run::<V, 2>(i, p),
409                3 => self.run::<V, 3>(i, p),
410                4 => self.run::<V, 4>(i, p),
411                _ => self.run::<V, 5>(i, p),
412            }
413        }
414    }
415
416    fn block_dispatch(&self, rows: (usize, usize), panels: (usize, usize)) {
417        #[cfg(target_arch = "aarch64")]
418        return self.block::<neon::Neon>(rows, panels);
419        #[cfg(target_arch = "x86_64")]
420        if has_fma() {
421            // SAFETY: the features block_fma enables were detected on this machine.
422            unsafe { self.block_fma(rows, panels) };
423            return;
424        }
425        #[allow(unreachable_code)]
426        self.block::<[f32; LANES]>(rows, panels);
427    }
428
429    #[cfg(target_arch = "x86_64")]
430    #[target_feature(enable = "avx2,fma")]
431    fn block_fma(&self, rows: (usize, usize), panels: (usize, usize)) {
432        self.block::<avx::Avx>(rows, panels);
433    }
434}
435
436/// `y = x wᵀ + b` with `x` as `[m, k]`, `w` as `[n, k]`, `b` as `[n]` and `y` as `[m, n]`. This
437/// packs `w` on every call, so a caller with a fixed weight should [`pack`] it once and run a
438/// [`Gemm`].
439///
440/// # Panics
441///
442/// If a length does not match the shape.
443#[allow(clippy::too_many_arguments)]
444pub fn linear(
445    x: &[f32],
446    m: usize,
447    k: usize,
448    w: &[f32],
449    n: usize,
450    b: Option<&[f32]>,
451    y: &mut [f32],
452    threads: usize,
453) {
454    let w = pack(w, n, k);
455    let g = Gemm { x, m, k, w: &w, n, b, ep: Epilogue::None };
456    let len = scratch_len(k, n);
457    g.run(y, threads, |tasks, f| par::for_each(tasks, threads, |t| f(t, &mut vec![0.0; len])));
458}
459
460/// One GEMM with its epilogue, `y = ep(x wᵀ + b)`, split into tiles that any thread may run.
461/// Every output element is computed the same way whatever the split, so the split is free to
462/// follow the thread count.
463#[derive(Debug, Clone, Copy)]
464pub struct Gemm<'a> {
465    /// `[m, k]`.
466    pub x: &'a [f32],
467    /// Rows of `x` and `y`.
468    pub m: usize,
469    /// The reduction length.
470    pub k: usize,
471    /// `[n, k]` as [`pack`] lays it out.
472    pub w: &'a [f32],
473    /// Columns of `y`.
474    pub n: usize,
475    /// `[n]`.
476    pub b: Option<&'a [f32]>,
477    /// What happens to each result.
478    pub ep: Epilogue,
479}
480
481impl Gemm<'_> {
482    /// Rows per task: the largest block that still gives every thread a few tasks.
483    #[cfg(not(target_os = "macos"))]
484    fn row_block(&self, threads: usize) -> usize {
485        let nt = self.n.div_ceil(NB);
486        [MB, 12 * MR, 6 * MR, 3 * MR]
487            .into_iter()
488            .find(|&mb| self.m.div_ceil(mb) * nt >= 3 * threads)
489            .unwrap_or(MR)
490    }
491
492    /// Runs the GEMM into `y`, handing `spawn` a task count and the task body to run for each.
493    /// A task gets [`scratch_len`] floats of scratch of its own.
494    ///
495    /// # Panics
496    ///
497    /// If a length does not match the shape.
498    pub fn run(
499        &self,
500        y: &mut [f32],
501        threads: usize,
502        spawn: impl FnOnce(usize, &(dyn Fn(usize, &mut [f32]) + Sync)),
503    ) {
504        let Self { x, m, k, w, n, b, ep } = *self;
505        assert_eq!(x.len(), m * k, "x is not [m, k]");
506        assert_eq!(w.len(), packed_len(n, k), "w is not [n, k] packed");
507        assert_eq!(y.len(), m * n, "y is not [m, n]");
508        if let Some(b) = b {
509            assert_eq!(b.len(), n, "b is not [n]");
510        }
511        if m == 0 || n == 0 {
512            return;
513        }
514        let shared = Shared::new(y);
515        let args = Args { x, w, b, ep, k, n, y: &shared };
516        if k == 0 {
517            for i in 0..m {
518                for j in 0..n {
519                    args.put(i, j, 0.0);
520                }
521            }
522            return;
523        }
524        #[cfg(target_os = "macos")]
525        {
526            blas::run(&args, m, threads, spawn);
527        }
528        #[cfg(not(target_os = "macos"))]
529        self.run_panels(&args, threads, spawn);
530    }
531
532    #[cfg(not(target_os = "macos"))]
533    fn run_panels(
534        &self,
535        args: &Args<'_>,
536        threads: usize,
537        spawn: impl FnOnce(usize, &(dyn Fn(usize, &mut [f32]) + Sync)),
538    ) {
539        let (m, n) = (self.m, self.n);
540        let mb = self.row_block(threads);
541        let mt = m.div_ceil(mb);
542        let (panels, per) = (n.div_ceil(NR), NB / NR);
543        let nt = panels.div_ceil(per);
544        spawn(mt * nt, &|t, _| {
545            let (bi, bj) = (t % mt, t / mt);
546            let rows = (bi * mb, ((bi + 1) * mb).min(m));
547            args.block_dispatch(rows, (bj * per, ((bj + 1) * per).min(panels)));
548        });
549    }
550}
551
552#[cfg(target_os = "macos")]
553mod blas {
554    use super::Args;
555
556    /// Rows per call. The bits of a row depend on the row count of the call, so it never changes.
557    pub(super) const ROWS: usize = 64;
558    /// Elements of `k` per call. Accelerate sums in f32, so the calls cover `k` in blocks of this
559    /// and their results are added in f64, which keeps long rows as accurate as the NEON kernel.
560    const KB: usize = 128;
561    /// Columns per task are a multiple of this when the columns are split.
562    pub(super) const COLS: usize = 256;
563    /// Outputs narrower than this skip Accelerate. On the M1 it gives a lone output column
564    /// different bits by where the row sits in the call, so a question would not get the same
565    /// answer alone and in a batch. A few dot products per row cost nothing anyway.
566    const NARROW: usize = 8;
567
568    #[link(name = "Accelerate", kind = "framework")]
569    unsafe extern "C" {
570        fn cblas_sgemm(
571            order: i32,
572            trans_a: i32,
573            trans_b: i32,
574            m: i32,
575            n: i32,
576            k: i32,
577            alpha: f32,
578            a: *const f32,
579            lda: i32,
580            b: *const f32,
581            ldb: i32,
582            beta: f32,
583            c: *mut f32,
584            ldc: i32,
585        );
586    }
587
588    /// `len` f64 values inside `buf`, which holds at least `2 len + 1` f32 values.
589    fn f64s(buf: &mut [f32], len: usize) -> &mut [f64] {
590        // SAFETY: any bit pattern is an f64, and align_to_mut only hands out aligned values.
591        let (_, mid, _) = unsafe { buf.align_to_mut::<f64>() };
592        &mut mid[..len]
593    }
594
595    const ROW_MAJOR: i32 = 101;
596    const NO_TRANS: i32 = 111;
597    const TRANS: i32 = 112;
598
599    /// One task per block of 64 rows of `x`, each a few `sgemm` calls into the task's scratch,
600    /// then the bias and the epilogue on the way to `y`.
601    ///
602    /// The columns are split in chunks of 256 whatever the number of rows, since Accelerate picks
603    /// its kernels by the width of the call and a width that followed the batch would change bits.
604    pub(super) fn run(
605        args: &Args<'_>,
606        m: usize,
607        threads: usize,
608        spawn: impl FnOnce(usize, &(dyn Fn(usize, &mut [f32]) + Sync)),
609    ) {
610        let (k, n) = (args.k, args.n);
611        let dim = |v: usize| i32::try_from(v).expect("GEMM sizes fit in an i32");
612        let _ = threads;
613        if n < NARROW {
614            spawn(m.div_ceil(ROWS), &|t, _| {
615                let mut sums = [0f32; NARROW];
616                for r in t * ROWS..m.min((t + 1) * ROWS) {
617                    let x = &args.x[r * k..(r + 1) * k];
618                    for (j, v) in sums[..n].iter_mut().enumerate() {
619                        let w = &args.w[j * k..(j + 1) * k];
620                        *v = x
621                            .iter()
622                            .zip(w)
623                            .map(|(&a, &b)| f64::from(a) * f64::from(b))
624                            .sum::<f64>() as f32;
625                    }
626                    args.put_row(r, 0, &sums[..n]);
627                }
628            });
629            return;
630        }
631        let (mt, cols) = (m.div_ceil(ROWS), COLS.min(n));
632        spawn(mt * n.div_ceil(cols), &|t, scratch| {
633            let (bi, bj) = (t % mt, t / mt);
634            let (r0, rows) = (bi * ROWS, ROWS.min(m - bi * ROWS));
635            let (j0, nc) = (bj * cols, cols.min(n - bj * cols));
636            let (xs, rest) = scratch[..ROWS * (k + 3 * cols) + 1].split_at_mut(ROWS * k);
637            let (c, wide) = rest.split_at_mut(ROWS * cols);
638            let (c, wide) = (&mut c[..ROWS * nc], &mut f64s(wide, ROWS * cols)[..ROWS * nc]);
639            wide.fill(0.0);
640            let x = if rows == ROWS {
641                &args.x[r0 * k..(r0 + ROWS) * k]
642            } else {
643                xs[..rows * k].copy_from_slice(&args.x[r0 * k..(r0 + rows) * k]);
644                xs[rows * k..].fill(0.0);
645                &xs[..]
646            };
647            let mut p = 0;
648            while p < k {
649                let kc = KB.min(k - p);
650                // SAFETY: x is ROWS by k and w is n by k, read from column p for kc columns, and
651                // c is ROWS by n, all row major and dense.
652                unsafe {
653                    cblas_sgemm(
654                        ROW_MAJOR,
655                        NO_TRANS,
656                        TRANS,
657                        dim(ROWS),
658                        dim(nc),
659                        dim(kc),
660                        1.0,
661                        x.as_ptr().add(p),
662                        dim(k),
663                        args.w.as_ptr().add(j0 * k + p),
664                        dim(k),
665                        0.0,
666                        c.as_mut_ptr(),
667                        dim(nc),
668                    );
669                }
670                wide.iter_mut().zip(c.iter()).for_each(|(w, &v)| *w += f64::from(v));
671                p += kc;
672            }
673            c.iter_mut().zip(wide.iter()).for_each(|(c, &w)| *c = w as f32);
674            for r in 0..rows {
675                args.put_row(r0 + r, j0, &c[r * nc..(r + 1) * nc]);
676            }
677        });
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684    use crate::testing::{Rng, close};
685
686    fn naive(x: &[f32], m: usize, k: usize, w: &[f32], n: usize, b: Option<&[f32]>) -> Vec<f32> {
687        let mut y = vec![0f32; m * n];
688        for i in 0..m {
689            for j in 0..n {
690                let s: f64 =
691                    (0..k).map(|p| f64::from(x[i * k + p]) * f64::from(w[j * k + p])).sum();
692                y[i * n + j] = (s + b.map_or(0.0, |b| f64::from(b[j]))) as f32;
693            }
694        }
695        y
696    }
697
698    #[test]
699    fn matches_naive_on_awkward_shapes() {
700        let mut rng = Rng(7);
701        let shapes = [
702            (0, 8, 5),
703            (1, 1, 1),
704            (1, 7, 3),
705            (3, 16, 2),
706            (4, 9, 3),
707            (5, 64, 7),
708            (130, 33, 50),
709            (129, 1028, 49),
710            (17, 0, 4),
711        ];
712        for (m, k, n) in shapes {
713            let x = rng.vec(m * k);
714            let w = rng.vec(n * k);
715            let b = rng.vec(n);
716            for bias in [None, Some(&b[..])] {
717                let want = naive(&x, m, k, &w, n, bias);
718                for threads in [1, 3] {
719                    let mut y = vec![f32::NAN; m * n];
720                    linear(&x, m, k, &w, n, bias, &mut y, threads);
721                    // Accelerate sums in f32 alone, so it drifts a little further on long rows.
722                    let tol = if cfg!(target_os = "macos") { 2e-5 } else { 1e-5 };
723                    close(&y, &want, tol, &format!("{m}x{k}x{n}"));
724                }
725            }
726        }
727    }
728
729    #[test]
730    fn epilogues_on_every_column() {
731        let mut rng = Rng(5);
732        let (m, k, n) = (70, 40, 600);
733        let (x, w, b, y0) = (rng.vec(m * k), rng.vec(n * k), rng.vec(n), rng.vec(m * n));
734        let packed = pack(&w, n, k);
735        let lin = naive(&x, m, k, &w, n, Some(&b));
736        for ep in [Epilogue::None, Epilogue::Gelu, Epilogue::Relu, Epilogue::Accumulate] {
737            let want: Vec<f32> = lin
738                .iter()
739                .zip(&y0)
740                .map(|(&v, &y)| match ep {
741                    Epilogue::None => v,
742                    Epilogue::Gelu => gelu(v),
743                    Epilogue::Relu => v.max(0.0),
744                    Epilogue::Accumulate => y + v,
745                })
746                .collect();
747            let mut y = y0.clone();
748            let g = Gemm { x: &x, m, k, w: &packed, n, b: Some(&b), ep };
749            let len = scratch_len(k, n);
750            g.run(&mut y, 4, |tasks, f| par::for_each(tasks, 4, |t| f(t, &mut vec![0.0; len])));
751            close(&y, &want, 1e-4, &format!("{ep:?}"));
752        }
753    }
754
755    #[test]
756    fn same_bits_for_any_split() {
757        let mut rng = Rng(11);
758        let (m, k, n) = (137, 300, 600);
759        let x = rng.vec(m * k);
760        let w = rng.vec(n * k);
761        let mut one = vec![0f32; m * n];
762        linear(&x, m, k, &w, n, None, &mut one, 1);
763        for threads in [2, 5, 10, 16] {
764            let mut y = vec![0f32; m * n];
765            linear(&x, m, k, &w, n, None, &mut y, threads);
766            assert!(y.iter().zip(&one).all(|(a, b)| a.to_bits() == b.to_bits()));
767        }
768        // And the same bits for a row wherever it sits in the batch.
769        for i in [0, 5, 70, 136] {
770            let mut row = vec![0f32; n];
771            linear(&x[i * k..(i + 1) * k], 1, k, &w, n, None, &mut row, 1);
772            assert!(row.iter().zip(&one[i * n..]).all(|(a, b)| a.to_bits() == b.to_bits()));
773        }
774    }
775
776    #[test]
777    fn dot_matches_naive() {
778        let mut rng = Rng(3);
779        for k in [0, 1, 7, 8, 64, 65, 200] {
780            let (a, b) = (rng.vec(k), rng.vec(k));
781            let want = naive(&a, 1, k, &b, 1, None);
782            close(&[dot(&a, &b)], &want, 1e-5, &format!("dot {k}"));
783        }
784    }
785}