#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
use ndarray::parallel::prelude::IntoParallelIterator;
#[cfg(any(feature = "machine_learning", feature = "utils"))]
use ndarray::{Array1, Ix1};
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
use ndarray::{Array2, ArrayBase, Axis, Data, Ix2, LinalgScalar};
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
const PAR_ROWSPLIT_MIN_BLOCK: usize = 8;
tunable_gate! {
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
pub(crate) GEMM_COLPAR_MIN_COLS_PER_THREAD
=> gemm_colpar_min_cols_per_thread / set_gemm_colpar_min_cols_per_thread = 16
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
pub(crate) trait MatmulElem: LinalgScalar + Send + Sync {
fn gemm_rayon_min_flops() -> usize;
#[cfg(any(feature = "machine_learning", feature = "utils"))]
fn gemv_rayon_min_flops() -> usize;
}
tunable_gate! {
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
pub(crate) GEMM_RAYON_MIN_FLOPS_F32
=> gemm_rayon_min_flops_f32 / set_gemm_rayon_min_flops_f32 = 8_000_000
}
tunable_gate! {
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
pub(crate) GEMM_RAYON_MIN_FLOPS_F64
=> gemm_rayon_min_flops_f64 / set_gemm_rayon_min_flops_f64 = 1_000_000
}
tunable_gate! {
#[cfg(any(feature = "machine_learning", feature = "utils"))]
pub(crate) GEMV_RAYON_MIN_FLOPS_F32
=> gemv_rayon_min_flops_f32 / set_gemv_rayon_min_flops_f32 = 524_288
}
tunable_gate! {
#[cfg(any(feature = "machine_learning", feature = "utils"))]
pub(crate) GEMV_RAYON_MIN_FLOPS_F64
=> gemv_rayon_min_flops_f64 / set_gemv_rayon_min_flops_f64 = 524_288
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
impl MatmulElem for f32 {
fn gemm_rayon_min_flops() -> usize {
gemm_rayon_min_flops_f32()
}
#[cfg(any(feature = "machine_learning", feature = "utils"))]
fn gemv_rayon_min_flops() -> usize {
gemv_rayon_min_flops_f32()
}
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
impl MatmulElem for f64 {
fn gemm_rayon_min_flops() -> usize {
gemm_rayon_min_flops_f64()
}
#[cfg(any(feature = "machine_learning", feature = "utils"))]
fn gemv_rayon_min_flops() -> usize {
gemv_rayon_min_flops_f64()
}
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
fn gemm_kernel<T, S1, S2>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
par: gemm::Parallelism,
) -> Array2<T>
where
T: LinalgScalar,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (m, k) = a.dim();
let n = b.ncols();
let mut out = Array2::<T>::zeros((m, n));
if m == 0 || n == 0 || k == 0 {
return out;
}
let a_strides = a.strides();
let b_strides = b.strides();
unsafe {
gemm::gemm(
m,
n,
k,
out.as_mut_ptr(),
1,
n as isize,
false,
a.as_ptr(),
a_strides[1],
a_strides[0],
b.as_ptr(),
b_strides[1],
b_strides[0],
T::zero(),
T::one(),
false,
false,
false,
par,
);
}
out
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
fn gemm_rowsplit<T, S1, S2>(a: &ArrayBase<S1, Ix2>, b: &ArrayBase<S2, Ix2>) -> Array2<T>
where
T: MatmulElem,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (m, _k) = a.dim();
let n = b.ncols();
let threads = rayon::current_num_threads();
let chunk = m.div_ceil(threads.max(1)).max(PAR_ROWSPLIT_MIN_BLOCK);
if chunk >= m {
return gemm_kernel(a, b, gemm::Parallelism::None);
}
let bv = b.view();
let mut out = Array2::<T>::zeros((m, n));
out.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(&gemm_kernel(&a_blk, &bv, gemm::Parallelism::None));
});
out
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
fn gemm_par_strategy<T, S1, S2>(a: &ArrayBase<S1, Ix2>, b: &ArrayBase<S2, Ix2>) -> Array2<T>
where
T: MatmulElem,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (m, _k) = a.dim();
let n = b.ncols();
let threads = rayon::current_num_threads();
if m >= n && n < threads.saturating_mul(gemm_colpar_min_cols_per_thread()) {
gemm_rowsplit(a, b)
} else {
gemm_kernel(a, b, gemm::Parallelism::Rayon(threads))
}
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
pub(crate) fn gemm_par_switch<T, S1, S2>(
a: &ArrayBase<S1, Ix2>,
b: &ArrayBase<S2, Ix2>,
parallel: bool,
) -> Array2<T>
where
T: MatmulElem,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (m, k) = a.dim();
let (kb, n) = b.dim();
assert_eq!(
k, kb,
"gemm_par_switch: inner dimensions disagree (a is {m}x{k}, b is {kb}x{n})",
);
if parallel {
gemm_par_strategy(a, b)
} else {
gemm_kernel(a, b, gemm::Parallelism::None)
}
}
#[cfg(any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
))]
pub(crate) fn gemm_par_auto<T, S1, S2>(a: &ArrayBase<S1, Ix2>, b: &ArrayBase<S2, Ix2>) -> Array2<T>
where
T: MatmulElem,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (m, k) = a.dim();
let n = b.ncols();
let flops = 2usize.saturating_mul(m).saturating_mul(k).saturating_mul(n);
gemm_par_switch(a, b, flops >= T::gemm_rayon_min_flops())
}
#[cfg(any(feature = "machine_learning", feature = "utils"))]
fn gemv_par_strategy<T, S1, S2>(a: &ArrayBase<S1, Ix2>, x: &ArrayBase<S2, Ix1>) -> Array1<T>
where
T: MatmulElem,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let x_col = x.view().insert_axis(Axis(1)); gemm_rowsplit(a, &x_col).index_axis_move(Axis(1), 0)
}
#[cfg(any(feature = "machine_learning", feature = "utils"))]
pub(crate) fn gemv_par_switch<T, S1, S2>(
a: &ArrayBase<S1, Ix2>,
x: &ArrayBase<S2, Ix1>,
parallel: bool,
) -> Array1<T>
where
T: MatmulElem,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (m, k) = a.dim();
assert_eq!(
k,
x.len(),
"gemv_par_switch: inner dimensions disagree (a is {m}x{k}, x has length {})",
x.len()
);
if parallel {
gemv_par_strategy(a, x)
} else {
let x_col = x.view().insert_axis(Axis(1)); gemm_kernel(a, &x_col, gemm::Parallelism::None).index_axis_move(Axis(1), 0)
}
}
#[cfg(any(feature = "machine_learning", feature = "utils"))]
pub(crate) fn gemv_par_auto<T, S1, S2>(a: &ArrayBase<S1, Ix2>, x: &ArrayBase<S2, Ix1>) -> Array1<T>
where
T: MatmulElem,
S1: Data<Elem = T>,
S2: Data<Elem = T>,
{
let (m, k) = a.dim();
let flops = 2usize.saturating_mul(m).saturating_mul(k);
gemv_par_switch(a, x, flops >= T::gemv_rayon_min_flops())
}
tunable_gate! {
pub(crate) GEMM_CHUNK_ELEMS => gemm_chunk_elems / set_gemm_chunk_elems = 33_554_432
}
#[doc(hidden)]
pub fn gemm_chunk_rows(row_len: usize) -> usize {
(gemm_chunk_elems() / row_len.max(1)).clamp(16, 4096)
}
tunable_gate! {
pub(crate) CACHE_RESIDENT_MAX_BYTES
=> cache_resident_max_bytes / set_cache_resident_max_bytes = 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()
}
#[cfg(all(
test,
any(
feature = "machine_learning",
feature = "neural_network",
feature = "utils"
)
))]
mod tests {
use super::*;
#[cfg(any(feature = "machine_learning", feature = "utils"))]
use ndarray::Array1;
use ndarray::{Array2, s};
fn rand_f32(r: usize, c: usize, seed: u64) -> Array2<f32> {
Array2::from_shape_fn((r, c), |(i, j)| {
let t = (seed as f64) * 0.731 + (i * c + j) as f64 * 0.618_033_988_7;
((t.sin() * 43758.5453).fract() - 0.5) as f32
})
}
fn rand_f64(r: usize, c: usize, seed: u64) -> Array2<f64> {
Array2::from_shape_fn((r, c), |(i, j)| {
let t = (seed as f64) * 0.731 + (i * c + j) as f64 * 0.618_033_988_7;
(t.sin() * 43758.5453).fract() - 0.5
})
}
fn naive<T: LinalgScalar>(a: &Array2<T>, b: &Array2<T>) -> Array2<T> {
let (m, k) = a.dim();
let n = b.ncols();
let mut c = Array2::<T>::zeros((m, n));
for i in 0..m {
for j in 0..n {
let mut acc = T::zero();
for p in 0..k {
acc = acc + a[[i, p]] * b[[p, j]];
}
c[[i, j]] = acc;
}
}
c
}
fn assert_close_f32(got: &Array2<f32>, want: &Array2<f32>, eps: f32) {
assert_eq!(got.shape(), want.shape());
for (g, w) in got.iter().zip(want.iter()) {
assert!((g - w).abs() <= eps, "f32 mismatch: {g} vs {w}");
}
}
fn assert_close_f64(got: &Array2<f64>, want: &Array2<f64>, eps: f64) {
assert_eq!(got.shape(), want.shape());
for (g, w) in got.iter().zip(want.iter()) {
assert!((g - w).abs() <= eps, "f64 mismatch: {g} vs {w}");
}
}
#[test]
fn gemm_par_auto_matches_reference_f32() {
for &(m, k, n) in &[
(17usize, 23usize, 19usize),
(128, 96, 128),
] {
let a = rand_f32(m, k, 1);
let b = rand_f32(k, n, 2);
assert_close_f32(&gemm_par_auto(&a, &b), &naive(&a, &b), 1e-2);
}
let a = rand_f32(256, 300, 3);
let b = rand_f32(300, 256, 4);
assert_close_f32(&gemm_par_auto(&a, &b), &a.dot(&b), 1e-2);
}
#[test]
fn gemm_par_auto_matches_reference_f64() {
for &(m, k, n) in &[
(17usize, 23usize, 19usize),
(64, 64, 64),
] {
let a = rand_f64(m, k, 1);
let b = rand_f64(k, n, 2);
assert_close_f64(&gemm_par_auto(&a, &b), &naive(&a, &b), 1e-9);
}
let a = rand_f64(128, 128, 3);
let b = rand_f64(128, 128, 4);
assert_close_f64(&gemm_par_auto(&a, &b), &a.dot(&b), 1e-9);
}
#[test]
fn gemm_par_auto_strided_operands() {
let a = rand_f64(40, 24, 5);
let b = rand_f64(40, 18, 6);
let got = gemm_par_auto(&a.t(), &b);
let want = naive(&a.t().to_owned(), &b);
assert_close_f64(&got, &want, 1e-9);
let a = rand_f64(40, 30, 7);
let b = rand_f64(30, 40, 8);
let a_sl = a.slice(s![..;2, ..]); let b_sl = b.slice(s![.., ..;2]); let got = gemm_par_auto(&a_sl, &b_sl);
let want = naive(&a_sl.to_owned(), &b_sl.to_owned());
assert_close_f64(&got, &want, 1e-9);
}
#[test]
fn gemm_par_auto_thin_output_rowsplit() {
let a = rand_f64(4096, 64, 61);
let b = rand_f64(64, 4, 62);
assert_close_f64(&gemm_par_auto(&a, &b), &a.dot(&b), 1e-9);
let a = rand_f32(16384, 64, 63);
let b = rand_f32(64, 4, 64);
assert_close_f32(&gemm_par_auto(&a, &b), &a.dot(&b), 1e-2);
}
#[test]
fn gemm_kernel_thread_count_independent_f64() {
for &(m, k, n) in &[(96usize, 96usize, 96usize), (256, 64, 64), (64, 8192, 64)] {
let a = rand_f64(m, k, 11);
let b = rand_f64(k, n, 12);
let serial = gemm_kernel(&a, &b, gemm::Parallelism::None);
for threads in [1usize, 2, 4, 8, 16, 32] {
let par = gemm_kernel(&a, &b, gemm::Parallelism::Rayon(threads));
assert!(
serial
.iter()
.zip(par.iter())
.all(|(s, p)| s.to_bits() == p.to_bits()),
"gemm f64 {m}x{k}x{n} differs between serial and Rayon({threads})"
);
}
}
}
#[test]
fn gemm_kernel_thread_count_independent_f32() {
for &(m, k, n) in &[(96usize, 96usize, 96usize), (64, 8192, 64)] {
let a = rand_f32(m, k, 13);
let b = rand_f32(k, n, 14);
let serial = gemm_kernel(&a, &b, gemm::Parallelism::None);
for threads in [1usize, 4, 32] {
let par = gemm_kernel(&a, &b, gemm::Parallelism::Rayon(threads));
assert!(
serial
.iter()
.zip(par.iter())
.all(|(s, p)| s.to_bits() == p.to_bits()),
"gemm f32 {m}x{k}x{n} differs between serial and Rayon({threads})"
);
}
}
}
#[test]
fn gemm_par_auto_run_to_run_deterministic() {
let a = rand_f64(200, 200, 21);
let b = rand_f64(200, 200, 22);
let c1 = gemm_par_auto(&a, &b);
let c2 = gemm_par_auto(&a, &b);
assert!(
c1.iter()
.zip(c2.iter())
.all(|(x, y)| x.to_bits() == y.to_bits())
);
}
#[cfg(any(feature = "machine_learning", feature = "utils"))]
#[test]
fn gemv_par_auto_matches_reference() {
for &(m, k) in &[(40usize, 24usize), (8192, 64) ] {
let a = rand_f64(m, k, 31);
let x = Array1::from_shape_fn(k, |i| ((i as f64) * 0.37).sin());
let got = gemv_par_auto(&a, &x);
let want = a.dot(&x);
assert_eq!(got.len(), want.len());
for (g, w) in got.iter().zip(want.iter()) {
assert!((g - w).abs() <= 1e-9, "gemv mismatch: {g} vs {w}");
}
}
}
#[cfg(any(feature = "machine_learning", feature = "utils"))]
#[test]
fn gemv_par_auto_rowsplit_matches_serial_numerically() {
let m = 8192; let k = 64;
let a = rand_f64(m, k, 41);
let x = Array1::from_shape_fn(k, |i| ((i as f64) * 0.59).sin());
let split = gemv_par_auto(&a, &x);
let serial = gemm_kernel(&a, &x.view().insert_axis(Axis(1)), gemm::Parallelism::None)
.index_axis_move(Axis(1), 0);
assert_eq!(split.len(), serial.len());
for (s, p) in split.iter().zip(serial.iter()) {
assert!(
(s - p).abs() <= 1e-10,
"gemv row split vs serial: {s} vs {p}"
);
}
}
#[cfg(any(feature = "machine_learning", feature = "utils"))]
#[test]
fn gemv_par_auto_run_to_run_deterministic() {
let a = rand_f64(8192, 64, 51);
let x = Array1::from_shape_fn(64, |i| ((i as f64) * 0.23).sin());
let y1 = gemv_par_auto(&a, &x);
let y2 = gemv_par_auto(&a, &x);
assert!(
y1.iter()
.zip(y2.iter())
.all(|(a, b)| a.to_bits() == b.to_bits())
);
}
#[test]
fn gemm_par_auto_edge_cases() {
let a = Array2::<f64>::zeros((0, 4));
let b = Array2::<f64>::zeros((4, 3));
assert_eq!(gemm_par_auto(&a, &b).shape(), &[0, 3]);
let a = Array2::<f64>::zeros((3, 0));
let b = Array2::<f64>::zeros((0, 4));
let c = gemm_par_auto(&a, &b);
assert_eq!(c.shape(), &[3, 4]);
assert!(c.iter().all(|&x| x == 0.0));
let a = Array2::<f64>::from_elem((1, 1), 3.0);
let b = Array2::<f64>::from_elem((1, 1), 4.0);
assert_eq!(gemm_par_auto(&a, &b)[[0, 0]], 12.0);
}
#[test]
#[should_panic]
fn gemm_par_auto_dimension_mismatch_panics() {
let a = Array2::<f64>::zeros((2, 3));
let b = Array2::<f64>::zeros((4, 2));
let _ = gemm_par_auto(&a, &b);
}
}