use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use numrs2::array::Array;
use scirs2_core::ndarray::{Array2, IxDyn};
use std::hint::black_box;
fn legacy_matmul_2d_blocked(a: &Array<f64>, b: &Array<f64>) -> Array<f64> {
let a_shape = a.shape();
let b_shape = b.shape();
let m = a_shape[0];
let k = a_shape[1];
let n = b_shape[1];
let mut c_data = vec![0.0f64; m * n];
let owned_a;
let a_data: &[f64] = match a.as_slice() {
Some(slice) => slice,
None => {
owned_a = a.to_vec();
&owned_a
}
};
let owned_b;
let b_data: &[f64] = match b.as_slice() {
Some(slice) => slice,
None => {
owned_b = b.to_vec();
&owned_b
}
};
const BLOCK_SIZE: usize = 64;
for i_block in (0..m).step_by(BLOCK_SIZE) {
for k_block in (0..k).step_by(BLOCK_SIZE) {
for j_block in (0..n).step_by(BLOCK_SIZE) {
let i_end = std::cmp::min(i_block + BLOCK_SIZE, m);
let k_end = std::cmp::min(k_block + BLOCK_SIZE, k);
let j_end = std::cmp::min(j_block + BLOCK_SIZE, n);
for i in i_block..i_end {
for k_l in k_block..k_end {
let a_ik = a_data[i * k + k_l];
for j in j_block..j_end {
c_data[i * n + j] += a_ik * b_data[k_l * n + j];
}
}
}
}
}
}
Array::from_vec(c_data).reshape(&[m, n])
}
fn legacy_matmul_2d_blocked_f32(a: &Array<f32>, b: &Array<f32>) -> Array<f32> {
let a_shape = a.shape();
let b_shape = b.shape();
let m = a_shape[0];
let k = a_shape[1];
let n = b_shape[1];
let mut c_data = vec![0.0f32; m * n];
let a_data = a.to_vec();
let b_data = b.to_vec();
const BLOCK_SIZE: usize = 64;
for i_block in (0..m).step_by(BLOCK_SIZE) {
for k_block in (0..k).step_by(BLOCK_SIZE) {
for j_block in (0..n).step_by(BLOCK_SIZE) {
let i_end = std::cmp::min(i_block + BLOCK_SIZE, m);
let k_end = std::cmp::min(k_block + BLOCK_SIZE, k);
let j_end = std::cmp::min(j_block + BLOCK_SIZE, n);
for i in i_block..i_end {
for k_l in k_block..k_end {
let a_ik = a_data[i * k + k_l];
for j in j_block..j_end {
c_data[i * n + j] += a_ik * b_data[k_l * n + j];
}
}
}
}
}
}
Array::from_vec(c_data).reshape(&[m, n])
}
fn legacy_batched_ixdyn(a: &Array<f64>, b: &Array<f64>) -> Array<f64> {
let a_shape = a.shape();
let b_shape = b.shape();
let batch_shape = &a_shape[..a_shape.len() - 2];
let m = a_shape[a_shape.len() - 2];
let k = a_shape[a_shape.len() - 1];
let n = b_shape[b_shape.len() - 1];
let mut output_shape = batch_shape.to_vec();
output_shape.push(m);
output_shape.push(n);
let mut result = Array::<f64>::zeros(&output_shape);
let batch_size: usize = batch_shape.iter().product();
for batch_idx in 0..batch_size {
let mut batch_indices = Vec::with_capacity(batch_shape.len());
let mut temp = batch_idx;
for &dim in batch_shape.iter().rev() {
batch_indices.insert(0, temp % dim);
temp /= dim;
}
let mut a_indices = batch_indices.clone();
a_indices.push(0);
a_indices.push(0);
let mut b_indices = batch_indices.clone();
b_indices.push(0);
b_indices.push(0);
for i in 0..m {
let a_idx_pos = a_indices.len() - 2;
a_indices[a_idx_pos] = i;
for j in 0..n {
let b_idx_pos = b_indices.len() - 1;
b_indices[b_idx_pos] = j;
let mut sum = 0.0f64;
for l in 0..k {
let a_col_pos = a_indices.len() - 1;
a_indices[a_col_pos] = l;
let b_row_pos = b_indices.len() - 2;
b_indices[b_row_pos] = l;
let a_val = a
.array()
.get(IxDyn(&a_indices))
.expect("batched element access should succeed");
let b_val = b
.array()
.get(IxDyn(&b_indices))
.expect("batched element access should succeed");
sum += a_val * b_val;
}
let mut output_indices = batch_indices.clone();
output_indices.push(i);
output_indices.push(j);
result
.set(&output_indices, sum)
.expect("batched output write should succeed");
}
}
}
result
}
fn seq_f64(len: usize) -> Vec<f64> {
(0..len).map(|i| (i as f64) * 0.125 - 3.0).collect()
}
fn seq_f32(len: usize) -> Vec<f32> {
(0..len).map(|i| (i as f32) * 0.125 - 3.0).collect()
}
fn mat_f64(m: usize, n: usize) -> Array<f64> {
Array::from_vec(seq_f64(m * n)).reshape(&[m, n])
}
fn mat_f32(m: usize, n: usize) -> Array<f32> {
Array::from_vec(seq_f32(m * n)).reshape(&[m, n])
}
fn gemm_flops(m: usize, k: usize, n: usize) -> u64 {
2 * (m as u64) * (k as u64) * (n as u64)
}
const M1_SHAPES: &[(usize, usize, usize)] = &[
(8, 8, 8),
(32, 32, 32),
(64, 64, 64),
(128, 128, 128),
(256, 256, 256),
(512, 512, 512),
(512, 64, 512),
];
fn bench_m1_matmul_2d_f64(c: &mut Criterion) {
let mut group = c.benchmark_group("M1/matmul_2d/f64");
group.sample_size(10);
for &(m, k, n) in M1_SHAPES {
let a = mat_f64(m, k);
let b = mat_f64(k, n);
let label = format!("{m}x{k}x{n}");
group.throughput(Throughput::Elements(gemm_flops(m, k, n)));
group.bench_with_input(
BenchmarkId::new("dispatched", &label),
&(&a, &b),
|bench, (a, b)| bench.iter(|| black_box(a.matmul(b).expect("matmul should succeed"))),
);
group.bench_with_input(
BenchmarkId::new("legacy_blocked", &label),
&(&a, &b),
|bench, (a, b)| bench.iter(|| black_box(legacy_matmul_2d_blocked(a, b))),
);
}
group.finish();
}
fn bench_m1_matmul_2d_f32(c: &mut Criterion) {
let mut group = c.benchmark_group("M1/matmul_2d/f32");
group.sample_size(10);
for &(m, k, n) in M1_SHAPES {
let a = mat_f32(m, k);
let b = mat_f32(k, n);
let label = format!("{m}x{k}x{n}");
group.throughput(Throughput::Elements(gemm_flops(m, k, n)));
group.bench_with_input(
BenchmarkId::new("dispatched", &label),
&(&a, &b),
|bench, (a, b)| bench.iter(|| black_box(a.matmul(b).expect("matmul should succeed"))),
);
group.bench_with_input(
BenchmarkId::new("legacy_blocked", &label),
&(&a, &b),
|bench, (a, b)| bench.iter(|| black_box(legacy_matmul_2d_blocked_f32(a, b))),
);
}
group.finish();
}
fn bench_m2_batched(c: &mut Criterion) {
let mut group = c.benchmark_group("M2/matmul_batched/f64");
group.sample_size(10);
for &batch in &[1usize, 8, 64] {
for &panel in &[16usize, 64] {
let a = Array::from_vec(seq_f64(batch * panel * panel)).reshape(&[batch, panel, panel]);
let b = Array::from_vec(seq_f64(batch * panel * panel)).reshape(&[batch, panel, panel]);
let label = format!("batch{batch}/panel{panel}");
group.throughput(Throughput::Elements(
batch as u64 * gemm_flops(panel, panel, panel),
));
group.bench_with_input(
BenchmarkId::new("dispatched", &label),
&(&a, &b),
|bench, (a, b)| {
bench.iter(|| black_box(a.matmul(b).expect("matmul should succeed")))
},
);
group.bench_with_input(
BenchmarkId::new("legacy_ixdyn", &label),
&(&a, &b),
|bench, (a, b)| bench.iter(|| black_box(legacy_batched_ixdyn(a, b))),
);
}
}
group.finish();
}
fn bench_m3_bakeoff(c: &mut Criterion) {
let mut group = c.benchmark_group("M3/bakeoff/f64");
group.sample_size(10);
for &size in &[128usize, 512] {
let a = mat_f64(size, size);
let b = mat_f64(size, size);
let a_nd = Array2::from_shape_vec((size, size), seq_f64(size * size))
.expect("shape should match data length");
let b_nd = Array2::from_shape_vec((size, size), seq_f64(size * size))
.expect("shape should match data length");
let label = format!("{size}x{size}");
group.throughput(Throughput::Elements(gemm_flops(size, size, size)));
group.bench_with_input(
BenchmarkId::new("kernels_gemm", &label),
&(&a, &b),
|bench, (a, b)| bench.iter(|| black_box(a.matmul(b).expect("matmul should succeed"))),
);
group.bench_with_input(
BenchmarkId::new("blas_accelerated", &label),
&(&a_nd, &b_nd),
|bench, (a_nd, b_nd)| {
bench.iter(|| {
black_box(
scirs2_linalg::blas_accelerated::matmul(&a_nd.view(), &b_nd.view())
.expect("matmul should succeed"),
)
})
},
);
}
group.finish();
}
criterion_group!(
benches,
bench_m1_matmul_2d_f64,
bench_m1_matmul_2d_f32,
bench_m2_batched,
bench_m3_bakeoff
);
criterion_main!(benches);