use fearless_simd::{Level, Simd, dispatch, f32x16, prelude::*};
use fearless_simd_macros::simd;
pub(super) const KC: usize = 256;
pub(super) const NC: usize = 512;
pub(super) const R: usize = 6;
pub(super) const W: usize = 16;
#[inline(always)]
#[allow(clippy::too_many_arguments)]
fn micro<S: Simd, const RR: usize>(
simd: S,
a: &[f32],
lda: usize,
b: &[f32],
ldb: usize,
c: &mut [f32],
ldc: usize,
klen: usize,
) {
let mut acc = [f32x16::splat(simd, 0.0); RR];
for (r, row) in acc.iter_mut().enumerate() {
*row = f32x16::from_slice(simd, &c[r * ldc..r * ldc + W]);
}
for p in 0..klen {
let bv = f32x16::from_slice(simd, &b[p * ldb..p * ldb + W]);
for (r, row) in acc.iter_mut().enumerate() {
let x = f32x16::splat(simd, a[r * lda + p]);
*row = x.mul_add_precise(bv, *row);
}
}
for (r, row) in acc.iter().enumerate() {
row.store_slice(&mut c[r * ldc..r * ldc + W]);
}
}
#[inline(always)]
#[allow(clippy::too_many_arguments)]
fn micro_rows<S: Simd>(
simd: S,
rows: usize,
a: &[f32],
lda: usize,
b: &[f32],
ldb: usize,
c: &mut [f32],
ldc: usize,
klen: usize,
) {
match rows {
1 => micro::<S, 1>(simd, a, lda, b, ldb, c, ldc, klen),
2 => micro::<S, 2>(simd, a, lda, b, ldb, c, ldc, klen),
3 => micro::<S, 3>(simd, a, lda, b, ldb, c, ldc, klen),
4 => micro::<S, 4>(simd, a, lda, b, ldb, c, ldc, klen),
5 => micro::<S, 5>(simd, a, lda, b, ldb, c, ldc, klen),
_ => micro::<S, 6>(simd, a, lda, b, ldb, c, ldc, klen),
}
}
fn direct_rows(b_len: usize, n: usize, j_full: usize, k0: usize, klen: usize) -> usize {
if b_len < j_full + W {
return 0;
}
((b_len - j_full - W) / n + 1).saturating_sub(k0).min(klen)
}
#[simd]
#[allow(clippy::too_many_arguments)]
fn matmul_rows_simd<S: Simd>(
simd: S,
a: &[f32],
b: &[f32],
k: usize,
n: usize,
c: &mut [f32],
kc: usize,
nc: usize,
) {
let m = c.len() / n;
let mut packed = Vec::new();
for j0 in (0..n).step_by(nc) {
let j1 = (j0 + nc).min(n);
let tail = (j1 - j0) % W;
let j_full = j1 - tail;
for k0 in (0..k).step_by(kc) {
let k1 = (k0 + kc).min(k);
let klen = k1 - k0;
let direct = if tail == 0 {
klen
} else {
direct_rows(b.len(), n, j_full, k0, klen)
};
packed.clear();
for p in direct..klen {
let from = (k0 + p) * n + j_full;
let mut row = [0.0f32; W];
row[..tail].copy_from_slice(&b[from..from + tail]);
packed.extend_from_slice(&row);
}
for i in (0..m).step_by(R) {
let rows = (m - i).min(R);
let a_block = &a[i * k + k0..];
for j in (j0..j_full).step_by(W) {
micro_rows(
simd,
rows,
a_block,
k,
&b[k0 * n + j..],
n,
&mut c[i * n + j..],
n,
klen,
);
}
if tail > 0 {
let mut edge = [0.0f32; R * W];
for r in 0..rows {
let from = (i + r) * n + j_full;
edge[r * W..r * W + tail].copy_from_slice(&c[from..from + tail]);
}
let b_tail = &b[k0 * n + j_full..];
micro_rows(simd, rows, a_block, k, b_tail, n, &mut edge, W, direct);
let a_rest = &a[i * k + k0 + direct..];
let rest = klen - direct;
micro_rows(simd, rows, a_rest, k, &packed, W, &mut edge, W, rest);
for r in 0..rows {
let to = (i + r) * n + j_full;
c[to..to + tail].copy_from_slice(&edge[r * W..r * W + tail]);
}
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn matmul_rows(
level: Level,
a: &[f32],
b: &[f32],
k: usize,
n: usize,
c: &mut [f32],
kc: usize,
nc: usize,
) {
dispatch!(level, simd => matmul_rows_simd(simd, a, b, k, n, c, kc, nc));
}
pub(super) fn matmul_tiled(
a: &[f32],
b: &[f32],
m: usize,
k: usize,
n: usize,
kc: usize,
nc: usize,
) -> Vec<f32> {
matmul_tiled_at(Level::new(), a, b, (m, k, n), (kc, nc))
}
pub(super) fn matmul_tiled_at(
level: Level,
a: &[f32],
b: &[f32],
(m, k, n): (usize, usize, usize),
(kc, nc): (usize, usize),
) -> Vec<f32> {
let mut c = vec![0.0f32; m * n];
matmul_rows(level, a, b, k, n, &mut c, kc, nc);
c
}
#[cfg(feature = "parallel")]
const ROWS_PER_TASK: usize = 16;
#[cfg(feature = "parallel")]
pub(super) const PARALLEL_MIN_MACS: usize = 1 << 21;
#[cfg(feature = "parallel")]
pub(super) fn use_threads(m: usize, k: usize, n: usize) -> bool {
m.saturating_mul(k).saturating_mul(n) >= PARALLEL_MIN_MACS
}
#[cfg(feature = "parallel")]
pub(super) fn matmul_parallel(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
use rayon::prelude::*;
let level = Level::new();
let mut c = vec![0.0f32; m * n];
c.par_chunks_mut(ROWS_PER_TASK * n)
.enumerate()
.for_each(|(task, rows)| {
let first = task * ROWS_PER_TASK;
matmul_rows(level, &a[first * k..], b, k, n, rows, KC, NC);
});
c
}
pub(super) fn matmul(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
#[cfg(feature = "parallel")]
if use_threads(m, k, n) {
return matmul_parallel(a, b, m, k, n);
}
matmul_tiled(a, b, m, k, n, KC, NC)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn direct_rows_counts_the_rows_whose_vector_fits() {
assert_eq!(direct_rows(50, 10, 0, 0, 5), 4);
assert_eq!(direct_rows(50, 10, 0, 2, 3), 2);
assert_eq!(direct_rows(50, 10, 0, 0, 2), 2);
assert_eq!(direct_rows(W, W, 0, 0, 1), 1);
assert_eq!(direct_rows(W - 1, W, 0, 0, 1), 0);
assert_eq!(direct_rows(50, 20, 8, 0, 2), 2);
assert_eq!(direct_rows(44, 20, 8, 0, 2), 2);
assert_eq!(direct_rows(43, 20, 8, 0, 2), 1);
assert_eq!(direct_rows(50, 10, 0, 4, 1), 0);
}
}