use ndarray::parallel::prelude::IntoParallelIterator;
use ndarray::{Array1, Array2, ArrayBase, Axis, Data, Ix1, Ix2, LinalgScalar};
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
const PAR_GEMM_MIN_BLOCK: usize = 64;
const PAR_GEMV_MIN_BLOCK: usize = 8;
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
pub(crate) trait MatmulElem: LinalgScalar + Send + Sync {
const PAR_GEMM_MIN_FLOPS: usize;
#[cfg(any(feature = "machine_learning", feature = "utils"))]
const PAR_GEMV_MIN_FLOPS: usize;
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
impl MatmulElem for f32 {
const PAR_GEMM_MIN_FLOPS: usize = 4_000_000;
#[cfg(any(feature = "machine_learning", feature = "utils"))]
const PAR_GEMV_MIN_FLOPS: usize = 524_288;
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
impl MatmulElem for f64 {
const PAR_GEMM_MIN_FLOPS: usize = 2_000_000;
#[cfg(any(feature = "machine_learning", feature = "utils"))]
const PAR_GEMV_MIN_FLOPS: usize = 524_288;
}
pub fn gemm<T, S1, S2>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
min_parallel_flops: usize,
) -> Array2<T>
where
T: LinalgScalar + Send + Sync,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (a, b) = (a.view(), b.view());
let (m, k) = a.dim();
let n = b.ncols();
let flops = 2usize.saturating_mul(m).saturating_mul(k).saturating_mul(n);
if flops < min_parallel_flops {
return into_standard_layout(a.dot(&b));
}
gemm_par(&a, &b, PAR_GEMM_MIN_BLOCK)
}
pub fn gemv<T, S1, S2>(
a: &ArrayBase<S1, Ix2>,
x: &ArrayBase<S2, Ix1>,
min_parallel_flops: usize,
) -> Array1<T>
where
T: LinalgScalar + Send + Sync,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (a, x) = (a.view(), x.view());
let (m, k) = a.dim();
let flops = 2usize.saturating_mul(m).saturating_mul(k);
if flops < min_parallel_flops {
return a.dot(&x);
}
gemv_par(&a, &x, PAR_GEMV_MIN_BLOCK)
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
pub(crate) fn gemm_internal<T, S1, S2>(a: &ArrayBase<S1, Ix2>, b: &ArrayBase<S2, Ix2>) -> Array2<T>
where
T: MatmulElem,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
gemm(a, b, T::PAR_GEMM_MIN_FLOPS)
}
#[cfg(any(feature = "machine_learning", feature = "utils"))]
pub(crate) fn gemv_internal<T, S1, S2>(a: &ArrayBase<S1, Ix2>, x: &ArrayBase<S2, Ix1>) -> Array1<T>
where
T: MatmulElem,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
gemv(a, x, T::PAR_GEMV_MIN_FLOPS)
}
const GEMM_CHUNK_ELEMS: usize = 33_554_432;
#[doc(hidden)]
pub fn gemm_chunk_rows(row_len: usize) -> usize {
(GEMM_CHUNK_ELEMS / row_len.max(1)).clamp(16, 4096)
}
const CACHE_RESIDENT_MAX_BYTES: usize = 64 * 1024 * 1024;
#[doc(hidden)]
pub fn cache_resident<T>(rows: usize, cols: usize) -> bool {
rows.saturating_mul(cols)
.saturating_mul(std::mem::size_of::<T>())
< CACHE_RESIDENT_MAX_BYTES
}
pub fn gemm_par<T, S1, S2>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
min_block: usize,
) -> Array2<T>
where
T: LinalgScalar + Send + Sync,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (a, b) = (a.view(), b.view());
let (m, _) = a.dim();
let n = b.ncols();
let threads = rayon::current_num_threads();
if m >= n {
let chunk = m.div_ceil(threads).max(min_block);
if chunk >= m {
return into_standard_layout(a.dot(&b));
}
let mut c = Array2::<T>::zeros((m, n));
c.axis_chunks_iter_mut(Axis(0), chunk)
.into_par_iter()
.zip(a.axis_chunks_iter(Axis(0), chunk).into_par_iter())
.for_each(|(mut c_blk, a_blk)| {
c_blk.assign(&a_blk.dot(&b));
});
c
} else {
let chunk = n.div_ceil(threads).max(min_block);
if chunk >= n {
return into_standard_layout(a.dot(&b));
}
let mut c = Array2::<T>::zeros((m, n));
c.axis_chunks_iter_mut(Axis(1), chunk)
.into_par_iter()
.zip(b.axis_chunks_iter(Axis(1), chunk).into_par_iter())
.for_each(|(mut c_blk, b_blk)| {
c_blk.assign(&a.dot(&b_blk));
});
c
}
}
pub fn gemv_par<T, S1, S2>(
a: &ArrayBase<S1, Ix2>,
x: &ArrayBase<S2, Ix1>,
min_block: usize,
) -> Array1<T>
where
T: LinalgScalar + Send + Sync,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (a, x) = (a.view(), x.view());
let (m, _) = a.dim();
let threads = rayon::current_num_threads();
let chunk = m.div_ceil(threads).max(min_block);
if chunk >= m {
return a.dot(&x);
}
let mut y = Array1::<T>::zeros(m);
y.axis_chunks_iter_mut(Axis(0), chunk)
.into_par_iter()
.zip(a.axis_chunks_iter(Axis(0), chunk).into_par_iter())
.for_each(|(mut y_blk, a_blk)| {
y_blk.assign(&a_blk.dot(&x));
});
y
}
fn into_standard_layout<T: LinalgScalar>(m: Array2<T>) -> Array2<T> {
if m.is_standard_layout() {
m
} else {
m.as_standard_layout().into_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::Array2;
fn random_matrix(rows: usize, cols: usize, seed: u64) -> Array2<f32> {
Array2::from_shape_fn((rows, cols), |(i, j)| {
let t = (seed as f64) * 0.731 + (i * cols + j) as f64 * 0.618_033_988_7;
((t.sin() * 43758.5453).fract() - 0.5) as f32
})
}
fn random_matrix_f64(rows: usize, cols: usize, seed: u64) -> Array2<f64> {
Array2::from_shape_fn((rows, cols), |(i, j)| {
let t = (seed as f64) * 0.731 + (i * cols + j) as f64 * 0.618_033_988_7;
(t.sin() * 43758.5453).fract() - 0.5
})
}
#[test]
fn gemm_row_split_bitwise_equals_serial() {
let a = random_matrix(512, 64, 1);
let b = random_matrix(64, 256, 2);
let par = gemm(&a, &b, 0);
let serial = a.dot(&b);
assert_eq!(par.shape(), serial.shape());
assert!(
par.iter().zip(serial.iter()).all(|(x, y)| x == y),
"row-split parallel product must be bitwise identical to serial"
);
}
#[test]
fn gemm_column_split_bitwise_equals_serial() {
let a = random_matrix(64, 256, 3);
let b = random_matrix(256, 512, 4);
let par = gemm(&a, &b, 0);
let serial = a.dot(&b);
assert!(
par.iter().zip(serial.iter()).all(|(x, y)| x == y),
"column-split parallel product must be bitwise identical to serial"
);
}
#[test]
fn gemm_small_matches_serial() {
let a = random_matrix(8, 8, 5);
let b = random_matrix(8, 8, 6);
let par = gemm(&a, &b, usize::MAX);
let serial = a.dot(&b);
assert!(par.iter().zip(serial.iter()).all(|(x, y)| x == y));
}
#[test]
fn gemm_transposed_operands_match_serial() {
let x = random_matrix(2048, 64, 7);
let dz = random_matrix(2048, 64, 8);
let par = gemm(&x.t(), &dz, 0);
let serial = x.t().dot(&dz);
assert!(
par.iter().zip(serial.iter()).all(|(a, b)| a == b),
"transposed-operand product must match serial"
);
}
#[test]
fn gemm_f64_bitwise_equals_serial() {
let a = random_matrix_f64(512, 64, 9);
let b = random_matrix_f64(64, 256, 10);
let par = gemm(&a, &b, 0);
let serial = a.dot(&b);
assert!(
par.iter().zip(serial.iter()).all(|(x, y)| x == y),
"f64 row-split product must be bitwise identical to serial"
);
let a = random_matrix_f64(64, 256, 11);
let b = random_matrix_f64(256, 512, 12);
let par = gemm(&a, &b, 0);
let serial = a.dot(&b);
assert!(
par.iter().zip(serial.iter()).all(|(x, y)| x == y),
"f64 column-split product must be bitwise identical to serial"
);
}
#[test]
fn gemm_normalizes_column_major_dot_output() {
let a = Array2::<f32>::from_shape_vec((4, 1), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
let b_owned = Array2::<f32>::from_shape_vec((3, 1), vec![5.0, 6.0, 7.0]).unwrap();
let b = b_owned.t(); assert_eq!(b.strides(), &[1, 1]);
let c = gemm(&a, &b, usize::MAX);
assert!(c.is_standard_layout(), "output must be standard layout");
let serial = a.dot(&b);
assert!(c.iter().zip(serial.iter()).all(|(x, y)| x == y));
}
#[test]
fn gemv_split_bitwise_equals_serial() {
let a = random_matrix(4096, 1024, 13);
let x = random_matrix(1024, 1, 14).remove_axis(Axis(1));
let par = gemv(&a, &x, 0);
let serial = a.dot(&x);
assert_eq!(par.len(), serial.len());
assert!(
par.iter().zip(serial.iter()).all(|(p, s)| p == s),
"split matvec must be bitwise identical to serial"
);
}
#[test]
fn gemv_small_matches_serial() {
let a = random_matrix(16, 8, 15);
let x = random_matrix(8, 1, 16).remove_axis(Axis(1));
let par = gemv(&a, &x, usize::MAX);
let serial = a.dot(&x);
assert!(par.iter().zip(serial.iter()).all(|(p, s)| p == s));
}
#[test]
fn gemv_f64_bitwise_equals_serial() {
let a = random_matrix_f64(4096, 1024, 17);
let x = random_matrix_f64(1024, 1, 18).remove_axis(Axis(1));
let par = gemv(&a, &x, 0);
let serial = a.dot(&x);
assert!(
par.iter().zip(serial.iter()).all(|(p, s)| p == s),
"f64 split matvec must be bitwise identical to serial"
);
}
#[test]
fn gemm_zero_sized_axes() {
let a = Array2::<f32>::zeros((0, 4));
let b = Array2::<f32>::zeros((4, 3));
let c = gemm(&a, &b, 0);
assert_eq!(c.shape(), &[0, 3]);
let a = Array2::<f32>::zeros((3, 0));
let b = Array2::<f32>::zeros((0, 4));
let c = gemm(&a, &b, 0);
assert_eq!(c.shape(), &[3, 4]);
assert!(c.iter().all(|&x| x == 0.0));
}
#[test]
fn gemm_one_by_one() {
let a = Array2::<f32>::from_elem((1, 1), 3.0);
let b = Array2::<f32>::from_elem((1, 1), 4.0);
let c = gemm(&a, &b, 0);
assert_eq!(c[[0, 0]], 12.0);
}
#[test]
#[should_panic]
fn gemm_dimension_mismatch_panics() {
let a = Array2::<f32>::zeros((2, 3));
let b = Array2::<f32>::zeros((4, 2));
gemm(&a, &b, 0);
}
}