use metal::MTLResourceOptions;
use std::sync::Arc;
use super::buffers::{alloc_buf, download_f32, upload_f32};
use super::graph::MetalGraph;
#[test]
fn test_encode_gemm_tq2_matches_reference() {
use half::f16;
use oxibonsai_core::BlockTQ2_0_g128;
let graph = match MetalGraph::global() {
Ok(g) => g,
Err(_) => return,
};
let n_rows = 40usize; let k = 256usize; let blocks_per_row = k / 128;
let mut lcg: u32 = 0x1234_5678;
let mut next_code = || {
lcg = lcg.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((lcg >> 16) % 3) as u8
};
let mut blocks: Vec<BlockTQ2_0_g128> = Vec::with_capacity(n_rows * blocks_per_row);
for row in 0..n_rows {
for bk in 0..blocks_per_row {
let mut qs = [0u8; 32];
for b in qs.iter_mut() {
let c0 = next_code();
let c1 = next_code();
let c2 = next_code();
let c3 = next_code();
*b = c0 | (c1 << 2) | (c2 << 4) | (c3 << 6);
}
blocks.push(BlockTQ2_0_g128 {
qs,
d: f16::from_f32(0.0625 + 0.0078125 * (row as f32 + 0.5 * bk as f32)),
});
}
}
let mut dequant_w = vec![0f32; n_rows * k];
BlockTQ2_0_g128::dequant(&blocks, &mut dequant_w).expect("dequant reference weight failed");
let aos_bytes = {
let ptr = blocks.as_ptr() as *const u8;
let len = std::mem::size_of_val(blocks.as_slice());
unsafe { std::slice::from_raw_parts(ptr, len) }
};
let handle = graph
.get_or_upload_tq2_weight_soa(7_700_001u64, aos_bytes)
.expect("get_or_upload_tq2_weight_soa failed");
for &m in &[1usize, 7, 8, 9, 100] {
let input: Vec<f32> = (0..m * k)
.map(|i| {
let row = i / k;
let col = i % k;
((col as f32) * 0.013 - 0.4) + (row as f32) * 0.0007
})
.collect();
let mut got = vec![0f32; m * n_rows];
graph
.encode_gemm_tq2(&handle, &input, &mut got, m, n_rows, k)
.expect("encode_gemm_tq2 failed");
let mut expected = vec![0f32; m * n_rows];
for mm in 0..m {
for n in 0..n_rows {
let mut acc = 0f32;
for kk in 0..k {
acc += input[mm * k + kk] * dequant_w[n * k + kk];
}
expected[mm * n_rows + n] = acc;
}
}
let mut max_abs_err = 0f32;
for (idx, (a, b)) in expected.iter().zip(got.iter()).enumerate() {
let e = (a - b).abs();
if e > max_abs_err {
max_abs_err = e;
}
assert!(e < 1e-3, "M={m} idx={idx}: expected {a}, got {b} (|Δ|={e})");
}
let any_nonzero = got.iter().any(|&v| v.abs() > 1e-6);
assert!(any_nonzero, "M={m}: GEMM output is all-zero (suspicious)");
eprintln!("encode_gemm_tq2: M={m:>4} max_abs_err={max_abs_err:e}");
}
}
#[test]
fn test_encode_gemm_tq2_v8_tiled_parity() {
use half::f16;
use oxibonsai_core::BlockTQ2_0_g128;
use rayon::prelude::*;
let graph = match MetalGraph::global() {
Ok(g) => g,
Err(_) => return,
};
let ms = [1usize, 7, 8, 9, 32, 33, 100, 1536];
let ns = [8usize, 40, 3072];
let ks = [128usize, 256, 3072];
let mut weight_key: u64 = 7_700_100;
for &n_rows in &ns {
for &k in &ks {
let blocks_per_row = k / 128;
let mut lcg: u32 = 0x9E37_79B1 ^ ((n_rows as u32) << 8) ^ (k as u32);
let mut next_code = || {
lcg = lcg.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((lcg >> 16) % 3) as u8
};
let mut blocks: Vec<BlockTQ2_0_g128> = Vec::with_capacity(n_rows * blocks_per_row);
for row in 0..n_rows {
for bk in 0..blocks_per_row {
let mut qs = [0u8; 32];
for b in qs.iter_mut() {
let c0 = next_code();
let c1 = next_code();
let c2 = next_code();
let c3 = next_code();
*b = c0 | (c1 << 2) | (c2 << 4) | (c3 << 6);
}
blocks.push(BlockTQ2_0_g128 {
qs,
d: f16::from_f32(0.05 + 0.003 * (row % 17) as f32 + 0.002 * bk as f32),
});
}
}
let mut dequant_w = vec![0f32; n_rows * k];
BlockTQ2_0_g128::dequant(&blocks, &mut dequant_w)
.expect("dequant reference weight failed");
let aos_bytes = {
let ptr = blocks.as_ptr() as *const u8;
let len = std::mem::size_of_val(blocks.as_slice());
unsafe { std::slice::from_raw_parts(ptr, len) }
};
let handle = graph
.get_or_upload_tq2_weight_soa(weight_key, aos_bytes)
.expect("get_or_upload_tq2_weight_soa failed");
weight_key += 1;
for &m in &ms {
let input: Vec<f32> = (0..m * k)
.map(|i| {
let row = i / k;
let col = i % k;
((col as f32) * 0.011 - 0.37).sin() + (row as f32) * 0.0005
})
.collect();
let mut got = vec![0f32; m * n_rows];
graph
.encode_gemm_tq2(&handle, &input, &mut got, m, n_rows, k)
.expect("encode_gemm_tq2 (v8) failed");
let mut expected = vec![0f32; m * n_rows];
expected
.par_chunks_mut(n_rows)
.enumerate()
.for_each(|(mm, out_row)| {
let in_row = &input[mm * k..mm * k + k];
for (n, slot) in out_row.iter_mut().enumerate() {
let w_row = &dequant_w[n * k..n * k + k];
let mut acc = 0f32;
for kk in 0..k {
acc += in_row[kk] * w_row[kk];
}
*slot = acc;
}
});
let mut max_abs_err = 0f32;
for (idx, (a, b)) in expected.iter().zip(got.iter()).enumerate() {
let e = (a - b).abs();
if e > max_abs_err {
max_abs_err = e;
}
assert!(
e < 1e-3,
"v8 N={n_rows} K={k} M={m} idx={idx}: expected {a}, got {b} (|Δ|={e})"
);
}
if m * n_rows > 0 {
let any_nonzero = got.iter().any(|&v| v.abs() > 1e-6);
assert!(
any_nonzero,
"v8 N={n_rows} K={k} M={m}: GEMM output is all-zero (suspicious)"
);
}
eprintln!(
"encode_gemm_tq2 v8: N={n_rows:>4} K={k:>4} M={m:>4} max_abs_err={max_abs_err:e}"
);
}
}
}
}
#[test]
#[ignore = "benchmark: run explicitly with --ignored --nocapture"]
fn bench_gemm_tq2_v7_vs_v8_ratio() {
use half::f16;
use oxibonsai_core::BlockTQ2_0_g128;
use std::time::Instant;
let graph = match MetalGraph::global() {
Ok(g) => g,
Err(_) => {
eprintln!("no Metal device — skipping bench");
return;
}
};
let shapes = [(1536usize, 27648usize, 3072usize), (1536, 3072, 3072)];
const WARMUP: usize = 3;
const ITERS: usize = 11;
for (shape_idx, (m, n_rows, k)) in shapes.into_iter().enumerate() {
let bench_key: u64 = 7_800_000 + shape_idx as u64;
let blocks_per_row = k / 128;
let mut lcg: u32 = 0xDEAD_BEEF ^ (n_rows as u32);
let mut next_code = || {
lcg = lcg.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((lcg >> 16) % 3) as u8
};
let mut blocks: Vec<BlockTQ2_0_g128> = Vec::with_capacity(n_rows * blocks_per_row);
for _ in 0..n_rows * blocks_per_row {
let mut qs = [0u8; 32];
for b in qs.iter_mut() {
let c0 = next_code();
let c1 = next_code();
let c2 = next_code();
let c3 = next_code();
*b = c0 | (c1 << 2) | (c2 << 4) | (c3 << 6);
}
blocks.push(BlockTQ2_0_g128 {
qs,
d: f16::from_f32(0.0625),
});
}
let aos_bytes = {
let ptr = blocks.as_ptr() as *const u8;
let len = std::mem::size_of_val(blocks.as_slice());
unsafe { std::slice::from_raw_parts(ptr, len) }
};
let handle = graph
.get_or_upload_tq2_weight_soa(bench_key, aos_bytes)
.expect("upload weight failed");
let opts = MTLResourceOptions::StorageModeShared;
let input: Vec<f32> = (0..m * k)
.map(|i| ((i % 251) as f32) * 0.001 - 0.12)
.collect();
let input_buf = alloc_buf(
&graph.device,
std::mem::size_of_val(&input[..]) as u64,
opts,
)
.expect("alloc input failed");
unsafe { upload_f32(&input_buf, &input) };
let out_buf = alloc_buf(
&graph.device,
(m * n_rows * std::mem::size_of::<f32>()) as u64,
opts,
)
.expect("alloc output failed");
let run_once = |use_v8: bool| {
let cmd_buf = graph.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
if use_v8 {
graph.dispatch_gemm_tq2_v8(
encoder,
&handle.buffer,
&input_buf,
&out_buf,
n_rows as u32,
k as u32,
m as u32,
);
} else {
graph.dispatch_gemm_tq2_v7(
encoder,
&handle.buffer,
&input_buf,
&out_buf,
n_rows as u32,
k as u32,
m as u32,
);
}
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
};
let measure = |use_v8: bool| -> f64 {
for _ in 0..WARMUP {
run_once(use_v8);
}
let mut times = Vec::with_capacity(ITERS);
for _ in 0..ITERS {
let t0 = Instant::now();
run_once(use_v8);
times.push(t0.elapsed().as_secs_f64() * 1e3); }
times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
times[ITERS / 2]
};
let v7_ms = measure(false);
let v8_ms = measure(true);
let v7_ms2 = measure(false);
let v8_ms2 = measure(true);
let v7_med = (v7_ms + v7_ms2) / 2.0;
let v8_med = (v8_ms + v8_ms2) / 2.0;
let ratio = v7_med / v8_med;
eprintln!(
"BENCH M={m} N={n_rows} K={k}: v7={v7_med:.3}ms v8={v8_med:.3}ms speedup v7/v8={ratio:.2}x (median of {ITERS}, warmup {WARMUP}, 2 reps each)"
);
}
}
#[cfg(test)]
fn run_gemm_tq2_v9(
graph: &MetalGraph,
weight: &super::error::MetalWeightHandle,
input: &[f32],
m: usize,
n_rows: usize,
k: usize,
) -> Vec<f32> {
let opts = MTLResourceOptions::StorageModeShared;
let input_buf = alloc_buf(&graph.device, std::mem::size_of_val(input) as u64, opts)
.expect("alloc input failed");
unsafe { upload_f32(&input_buf, input) };
let out_buf = alloc_buf(
&graph.device,
(m * n_rows * std::mem::size_of::<f32>()) as u64,
opts,
)
.expect("alloc output failed");
let cmd_buf = graph.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
graph.dispatch_gemm_tq2_v9(
encoder,
&weight.buffer,
&input_buf,
&out_buf,
n_rows as u32,
k as u32,
m as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
let mut got = vec![0f32; m * n_rows];
unsafe { download_f32(&out_buf, &mut got) };
got
}
#[cfg(test)]
fn run_gemm_tq2_v10(
graph: &MetalGraph,
weight: &super::error::MetalWeightHandle,
input: &[f32],
m: usize,
n_rows: usize,
k: usize,
) -> Vec<f32> {
let opts = MTLResourceOptions::StorageModeShared;
let input_buf = alloc_buf(&graph.device, std::mem::size_of_val(input) as u64, opts)
.expect("alloc input failed");
unsafe { upload_f32(&input_buf, input) };
let out_buf = alloc_buf(
&graph.device,
(m * n_rows * std::mem::size_of::<f32>()) as u64,
opts,
)
.expect("alloc output failed");
let cmd_buf = graph.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
graph.dispatch_gemm_tq2_v10(
encoder,
&weight.buffer,
&input_buf,
&out_buf,
n_rows as u32,
k as u32,
m as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
let mut got = vec![0f32; m * n_rows];
unsafe { download_f32(&out_buf, &mut got) };
got
}
#[test]
fn test_gemm_tq2_v9_simdgroup_parity() {
use half::f16;
use oxibonsai_core::BlockTQ2_0_g128;
use rayon::prelude::*;
let graph = match MetalGraph::global() {
Ok(g) => g,
Err(_) => return,
};
let ms = [1usize, 7, 8, 9, 32, 33, 100, 1536];
let ns = [8usize, 40, 3072];
let ks = [128usize, 256, 3072];
let mut weight_key: u64 = 7_700_900;
for &n_rows in &ns {
for &k in &ks {
let blocks_per_row = k / 128;
let mut lcg: u32 = 0x2545_F491 ^ ((n_rows as u32) << 8) ^ (k as u32);
let mut next_code = || {
lcg = lcg.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((lcg >> 16) % 3) as u8
};
let mut blocks: Vec<BlockTQ2_0_g128> = Vec::with_capacity(n_rows * blocks_per_row);
for row in 0..n_rows {
for bk in 0..blocks_per_row {
let mut qs = [0u8; 32];
for b in qs.iter_mut() {
let c0 = next_code();
let c1 = next_code();
let c2 = next_code();
let c3 = next_code();
*b = c0 | (c1 << 2) | (c2 << 4) | (c3 << 6);
}
blocks.push(BlockTQ2_0_g128 {
qs,
d: f16::from_f32(0.05 + 0.003 * (row % 17) as f32 + 0.002 * bk as f32),
});
}
}
let mut dequant_w = vec![0f32; n_rows * k];
BlockTQ2_0_g128::dequant(&blocks, &mut dequant_w)
.expect("dequant reference weight failed");
let aos_bytes = {
let ptr = blocks.as_ptr() as *const u8;
let len = std::mem::size_of_val(blocks.as_slice());
unsafe { std::slice::from_raw_parts(ptr, len) }
};
let handle = graph
.get_or_upload_tq2_weight_soa(weight_key, aos_bytes)
.expect("get_or_upload_tq2_weight_soa failed");
weight_key += 1;
for &m in &ms {
let input: Vec<f32> = (0..m * k)
.map(|i| {
let row = i / k;
let col = i % k;
((col as f32) * 0.011 - 0.37).sin() + (row as f32) * 0.0005
})
.collect();
let got = run_gemm_tq2_v9(&graph, &handle, &input, m, n_rows, k);
let mut expected = vec![0f32; m * n_rows];
expected
.par_chunks_mut(n_rows)
.enumerate()
.for_each(|(mm, out_row)| {
let in_row = &input[mm * k..mm * k + k];
for (n, slot) in out_row.iter_mut().enumerate() {
let w_row = &dequant_w[n * k..n * k + k];
let mut acc = 0f32;
for kk in 0..k {
acc += in_row[kk] * w_row[kk];
}
*slot = acc;
}
});
let mut max_abs_err = 0f32;
for (idx, (a, b)) in expected.iter().zip(got.iter()).enumerate() {
let e = (a - b).abs();
if e > max_abs_err {
max_abs_err = e;
}
assert!(
e < 1e-3,
"v9 N={n_rows} K={k} M={m} idx={idx}: expected {a}, got {b} (|Δ|={e})"
);
}
if m * n_rows > 0 {
let any_nonzero = got.iter().any(|&v| v.abs() > 1e-6);
assert!(
any_nonzero,
"v9 N={n_rows} K={k} M={m}: GEMM output is all-zero (suspicious)"
);
}
eprintln!(
"gemm_tq2 v9: N={n_rows:>4} K={k:>4} M={m:>4} max_abs_err={max_abs_err:e}"
);
}
}
}
}
#[test]
fn test_gemm_tq2_v10_simdgroup_parity() {
use half::f16;
use oxibonsai_core::BlockTQ2_0_g128;
use rayon::prelude::*;
let graph = match MetalGraph::global() {
Ok(g) => g,
Err(_) => return,
};
let ms = [1usize, 7, 8, 9, 32, 33, 100, 1536];
let ns = [8usize, 40, 3072];
let ks = [128usize, 256, 3072];
let mut weight_key: u64 = 7_710_900;
for &n_rows in &ns {
for &k in &ks {
let blocks_per_row = k / 128;
let mut lcg: u32 = 0x2545_F491 ^ ((n_rows as u32) << 8) ^ (k as u32);
let mut next_code = || {
lcg = lcg.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((lcg >> 16) % 3) as u8
};
let mut blocks: Vec<BlockTQ2_0_g128> = Vec::with_capacity(n_rows * blocks_per_row);
for row in 0..n_rows {
for bk in 0..blocks_per_row {
let mut qs = [0u8; 32];
for b in qs.iter_mut() {
let c0 = next_code();
let c1 = next_code();
let c2 = next_code();
let c3 = next_code();
*b = c0 | (c1 << 2) | (c2 << 4) | (c3 << 6);
}
blocks.push(BlockTQ2_0_g128 {
qs,
d: f16::from_f32(0.05 + 0.003 * (row % 17) as f32 + 0.002 * bk as f32),
});
}
}
let mut dequant_w = vec![0f32; n_rows * k];
BlockTQ2_0_g128::dequant(&blocks, &mut dequant_w)
.expect("dequant reference weight failed");
let aos_bytes = {
let ptr = blocks.as_ptr() as *const u8;
let len = std::mem::size_of_val(blocks.as_slice());
unsafe { std::slice::from_raw_parts(ptr, len) }
};
let handle = graph
.get_or_upload_tq2_weight_soa(weight_key, aos_bytes)
.expect("get_or_upload_tq2_weight_soa failed");
weight_key += 1;
for &m in &ms {
let input: Vec<f32> = (0..m * k)
.map(|i| {
let row = i / k;
let col = i % k;
((col as f32) * 0.011 - 0.37).sin() + (row as f32) * 0.0005
})
.collect();
let got = run_gemm_tq2_v10(&graph, &handle, &input, m, n_rows, k);
let mut expected = vec![0f32; m * n_rows];
expected
.par_chunks_mut(n_rows)
.enumerate()
.for_each(|(mm, out_row)| {
let in_row = &input[mm * k..mm * k + k];
for (n, slot) in out_row.iter_mut().enumerate() {
let w_row = &dequant_w[n * k..n * k + k];
let mut acc = 0f32;
for kk in 0..k {
acc += in_row[kk] * w_row[kk];
}
*slot = acc;
}
});
let mut max_abs_err = 0f32;
for (idx, (a, b)) in expected.iter().zip(got.iter()).enumerate() {
let e = (a - b).abs();
if e > max_abs_err {
max_abs_err = e;
}
assert!(
e < 1e-3,
"v10 N={n_rows} K={k} M={m} idx={idx}: expected {a}, got {b} (|Δ|={e})"
);
}
if m * n_rows > 0 {
let any_nonzero = got.iter().any(|&v| v.abs() > 1e-6);
assert!(
any_nonzero,
"v10 N={n_rows} K={k} M={m}: GEMM output is all-zero (suspicious)"
);
}
eprintln!(
"gemm_tq2 v10: N={n_rows:>4} K={k:>4} M={m:>4} max_abs_err={max_abs_err:e}"
);
}
}
}
}
#[test]
#[ignore = "benchmark: run explicitly with --ignored --nocapture"]
fn bench_gemm_tq2_v9_ratio() {
use half::f16;
use oxibonsai_core::BlockTQ2_0_g128;
use std::time::Instant;
let graph = match MetalGraph::global() {
Ok(g) => g,
Err(_) => {
eprintln!("no Metal device — skipping bench");
return;
}
};
let shapes = [(1536usize, 27648usize, 3072usize), (1536, 3072, 3072)];
const WARMUP: usize = 3;
const ITERS: usize = 11;
for (shape_idx, (m, n_rows, k)) in shapes.into_iter().enumerate() {
let bench_key: u64 = 7_900_000 + shape_idx as u64;
let blocks_per_row = k / 128;
let mut lcg: u32 = 0xBADC_0FFE ^ (n_rows as u32);
let mut next_code = || {
lcg = lcg.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((lcg >> 16) % 3) as u8
};
let mut blocks: Vec<BlockTQ2_0_g128> = Vec::with_capacity(n_rows * blocks_per_row);
for _ in 0..n_rows * blocks_per_row {
let mut qs = [0u8; 32];
for b in qs.iter_mut() {
let c0 = next_code();
let c1 = next_code();
let c2 = next_code();
let c3 = next_code();
*b = c0 | (c1 << 2) | (c2 << 4) | (c3 << 6);
}
blocks.push(BlockTQ2_0_g128 {
qs,
d: f16::from_f32(0.0625),
});
}
let aos_bytes = {
let ptr = blocks.as_ptr() as *const u8;
let len = std::mem::size_of_val(blocks.as_slice());
unsafe { std::slice::from_raw_parts(ptr, len) }
};
let handle = graph
.get_or_upload_tq2_weight_soa(bench_key, aos_bytes)
.expect("upload weight failed");
let opts = MTLResourceOptions::StorageModeShared;
let input: Vec<f32> = (0..m * k)
.map(|i| ((i % 251) as f32) * 0.001 - 0.12)
.collect();
let input_buf = alloc_buf(
&graph.device,
std::mem::size_of_val(&input[..]) as u64,
opts,
)
.expect("alloc input failed");
unsafe { upload_f32(&input_buf, &input) };
let out_buf = alloc_buf(
&graph.device,
(m * n_rows * std::mem::size_of::<f32>()) as u64,
opts,
)
.expect("alloc output failed");
let run_once = |kernel: u8| {
let cmd_buf = graph.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
match kernel {
0 => graph.dispatch_gemm_tq2_v7(
encoder,
&handle.buffer,
&input_buf,
&out_buf,
n_rows as u32,
k as u32,
m as u32,
),
1 => graph.dispatch_gemm_tq2_v8(
encoder,
&handle.buffer,
&input_buf,
&out_buf,
n_rows as u32,
k as u32,
m as u32,
),
2 => graph.dispatch_gemm_tq2_v9(
encoder,
&handle.buffer,
&input_buf,
&out_buf,
n_rows as u32,
k as u32,
m as u32,
),
_ => graph.dispatch_gemm_tq2_v10(
encoder,
&handle.buffer,
&input_buf,
&out_buf,
n_rows as u32,
k as u32,
m as u32,
),
}
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
};
let measure = |kernel: u8| -> f64 {
for _ in 0..WARMUP {
run_once(kernel);
}
let mut times = Vec::with_capacity(ITERS);
for _ in 0..ITERS {
let t0 = Instant::now();
run_once(kernel);
times.push(t0.elapsed().as_secs_f64() * 1e3); }
times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
times[ITERS / 2]
};
let v7a = measure(0);
let v8a = measure(1);
let v9a = measure(2);
let v10a = measure(3);
let v7b = measure(0);
let v8b = measure(1);
let v9b = measure(2);
let v10b = measure(3);
let v7_med = (v7a + v7b) / 2.0;
let v8_med = (v8a + v8b) / 2.0;
let v9_med = (v9a + v9b) / 2.0;
let v10_med = (v10a + v10b) / 2.0;
let v8_over_v9 = v8_med / v9_med;
let v7_over_v9 = v7_med / v9_med;
let v9_over_v10 = v9_med / v10_med;
let v8_over_v10 = v8_med / v10_med;
let v7_over_v10 = v7_med / v10_med;
eprintln!(
"BENCH M={m} N={n_rows} K={k}: v7={v7_med:.3}ms v8={v8_med:.3}ms v9={v9_med:.3}ms v10={v10_med:.3}ms\n \
speedup v8/v9={v8_over_v9:.2}x v7/v9={v7_over_v9:.2}x || HEADLINE v9/v10={v9_over_v10:.3}x v8/v10={v8_over_v10:.2}x v7/v10={v7_over_v10:.2}x \
(median of {ITERS}, warmup {WARMUP}, 2 reps each)"
);
}
}
#[cfg(test)]
fn upload_ternary_weight_for_pool_test(
graph: &MetalGraph,
key: u64,
n_rows: usize,
k: usize,
) -> Arc<super::error::MetalWeightHandle> {
use half::f16;
use oxibonsai_core::BlockTQ2_0_g128;
let blocks_per_row = k / 128;
let mut lcg: u32 = 0x9E37_79B9 ^ ((n_rows as u32) << 7) ^ (k as u32);
let mut next_code = || {
lcg = lcg.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((lcg >> 16) % 3) as u8
};
let mut blocks: Vec<BlockTQ2_0_g128> = Vec::with_capacity(n_rows * blocks_per_row);
for row in 0..n_rows {
for bk in 0..blocks_per_row {
let mut qs = [0u8; 32];
for b in qs.iter_mut() {
let c0 = next_code();
let c1 = next_code();
let c2 = next_code();
let c3 = next_code();
*b = c0 | (c1 << 2) | (c2 << 4) | (c3 << 6);
}
blocks.push(BlockTQ2_0_g128 {
qs,
d: f16::from_f32(0.05 + 0.003 * (row % 13) as f32 + 0.002 * bk as f32),
});
}
}
let aos_bytes = {
let ptr = blocks.as_ptr() as *const u8;
let len = std::mem::size_of_val(blocks.as_slice());
unsafe { std::slice::from_raw_parts(ptr, len) }
};
graph
.get_or_upload_tq2_weight_soa(key, aos_bytes)
.expect("get_or_upload_tq2_weight_soa failed")
}
#[test]
fn test_gemm_pool_alloc_count_is_constant_after_warmup() {
let graph = match MetalGraph::global() {
Ok(g) => g,
Err(_) => return,
};
let make_input = |m: usize, k: usize| -> Vec<f32> {
(0..m * k)
.map(|i| {
let row = i / k;
let col = i % k;
((col as f32) * 0.009 - 0.31).cos() + (row as f32) * 0.0004
})
.collect()
};
let warm_n = 3072usize;
let warm_k = 3072usize;
let warm_m = 1536usize;
let warm_handle = upload_ternary_weight_for_pool_test(&graph, 7_701_400u64, warm_n, warm_k);
{
let input = make_input(warm_m, warm_k);
let mut out = vec![0f32; warm_m * warm_n];
graph
.encode_gemm_tq2(&warm_handle, &input, &mut out, warm_m, warm_n, warm_k)
.expect("encode_gemm_tq2 (pool warm) failed");
assert!(
out.iter().any(|&v| v.abs() > 1e-6),
"pool-warm GEMM output is all-zero (suspicious)"
);
}
let n_rows = 64usize;
let k = 256usize; let small_handle = upload_ternary_weight_for_pool_test(&graph, 7_701_500u64, n_rows, k);
let run_small = |m: usize| {
let input = make_input(m, k);
let mut out = vec![0f32; m * n_rows];
graph
.encode_gemm_tq2(&small_handle, &input, &mut out, m, n_rows, k)
.expect("encode_gemm_tq2 (pool steady) failed");
assert!(
out.iter().any(|&v| v.abs() > 1e-6),
"M={m}: pooled GEMM output is all-zero (suspicious)"
);
};
let before = MetalGraph::gemm_pool_alloc_count();
let steady = [1usize, 8, 9, 32, 100, 256, 512, 7, 64, 128, 512, 1];
for &m in &steady {
run_small(m);
}
let steady_allocs = MetalGraph::gemm_pool_alloc_count() - before;
eprintln!(
"gemm pool: {} steady-state matmuls (≤18 MB, warmed) → {steady_allocs} new buffer \
allocs (pre-pool would be {} = 2 per matmul)",
steady.len(),
steady.len() * 2,
);
assert_eq!(
steady_allocs,
0,
"warmed DiT-GEMM pool must trigger 0 new allocations across {} steady-state \
matmuls, but it grew {steady_allocs} time(s)",
steady.len()
);
}
#[test]
#[ignore = "benchmark: run explicitly with --ignored --nocapture"]
fn bench_gemm_pool_alloc_timing() {
use std::time::Instant;
let graph = match MetalGraph::global() {
Ok(g) => g,
Err(_) => {
eprintln!("no Metal device — skipping pool timing");
return;
}
};
let shapes = [(1536usize, 27648usize, 3072usize), (1536, 3072, 3072)];
const WARMUP: usize = 2;
const ITERS: usize = 7;
let opts = MTLResourceOptions::StorageModeShared;
for (idx, (m, n_rows, k)) in shapes.into_iter().enumerate() {
let handle = upload_ternary_weight_for_pool_test(&graph, 7_702_000 + idx as u64, n_rows, k);
let input: Vec<f32> = (0..m * k)
.map(|i| ((i % 251) as f32) * 0.001 - 0.12)
.collect();
let pooled_once = || {
let mut out = vec![0f32; m * n_rows];
graph
.encode_gemm_tq2(&handle, &input, &mut out, m, n_rows, k)
.expect("pooled encode_gemm_tq2 failed");
};
let nopool_once = || {
let in_buf = alloc_buf(
&graph.device,
std::mem::size_of_val(&input[..]) as u64,
opts,
)
.expect("alloc input failed");
let out_buf = alloc_buf(
&graph.device,
(m * n_rows * std::mem::size_of::<f32>()) as u64,
opts,
)
.expect("alloc output failed");
unsafe { upload_f32(&in_buf, &input) };
let cmd_buf = graph.command_queue.new_command_buffer();
let encoder = cmd_buf.new_compute_command_encoder();
graph.dispatch_gemm_tq2_v9(
encoder,
&handle.buffer,
&in_buf,
&out_buf,
n_rows as u32,
k as u32,
m as u32,
);
encoder.end_encoding();
cmd_buf.commit();
cmd_buf.wait_until_completed();
let mut got = vec![0f32; m * n_rows];
unsafe { download_f32(&out_buf, &mut got) };
};
let measure = |mut f: Box<dyn FnMut()>| -> f64 {
for _ in 0..WARMUP {
f();
}
let mut times = Vec::with_capacity(ITERS);
for _ in 0..ITERS {
let t0 = Instant::now();
f();
times.push(t0.elapsed().as_secs_f64() * 1e3);
}
times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
times[ITERS / 2]
};
let pool_a = measure(Box::new(pooled_once));
let nopool_a = measure(Box::new(nopool_once));
let pool_b = measure(Box::new(pooled_once));
let nopool_b = measure(Box::new(nopool_once));
let pool_med = (pool_a + pool_b) / 2.0;
let nopool_med = (nopool_a + nopool_b) / 2.0;
eprintln!(
"POOL-TIMING M={m} N={n_rows} K={k}: pooled={pool_med:.3}ms nopool={nopool_med:.3}ms \
nopool/pool={:.3}x saved/call≈{:.3}ms (median of {ITERS}, warmup {WARMUP}, 2 reps)",
nopool_med / pool_med,
nopool_med - pool_med,
);
}
}