#![allow(dead_code)]
use std::sync::{Arc, Mutex, MutexGuard};
use tessl::tensor::{bf16_bits_to_f32, f32_slice_to_bf16, GpuBuffer};
use tessl::{GpuRuntime, Tensor};
static GPU: Mutex<()> = Mutex::new(());
fn gpu_lock() -> MutexGuard<'static, ()> {
GPU.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn with_gpu<R>(f: impl FnOnce(&Arc<GpuRuntime>) -> R) -> R {
let _guard = gpu_lock();
let rt = GpuRuntime::new().expect("GpuRuntime::new (needs a Metal 4 device + built metallib)");
f(&rt)
}
pub fn with_two_gpus<R>(f: impl FnOnce(&Arc<GpuRuntime>, &Arc<GpuRuntime>) -> R) -> R {
let _guard = gpu_lock();
let a = GpuRuntime::new().expect("GpuRuntime::new (first)");
let b = GpuRuntime::new().expect("GpuRuntime::new (second)");
f(&a, &b)
}
pub struct Rng(u64);
impl Rng {
pub fn new(seed: u64) -> Self {
Rng(seed ^ 0x9e37_79b9_7f4a_7c15)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
pub fn unit(&mut self) -> f32 {
((self.next_u64() >> 40) as f32) / 8_388_608.0 - 1.0
}
}
pub fn random_f32(n: usize, seed: u64) -> Vec<f32> {
let mut rng = Rng::new(seed);
(0..n).map(|_| rng.unit()).collect()
}
pub fn round_trip_bf16(data: &[f32]) -> Vec<f32> {
f32_slice_to_bf16(data)
.into_iter()
.map(bf16_bits_to_f32)
.collect()
}
pub fn tensor_f32(rt: &Arc<GpuRuntime>, shape: &[usize], data: &[f32]) -> Tensor {
let t = rt.alloc_tensor_f32(shape).expect("alloc_tensor_f32");
t.buffer.write_f32(data);
t
}
pub fn tensor_bf16(rt: &Arc<GpuRuntime>, shape: &[usize], data: &[f32]) -> Tensor {
let t = rt.alloc_tensor_bf16(shape).expect("alloc_tensor_bf16");
t.buffer.write_bf16_bits(&f32_slice_to_bf16(data));
t
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Layout {
Nn,
Tn,
Nt,
}
pub struct Reference {
pub c: Vec<f64>,
pub mag: Vec<f64>,
}
pub fn reference(layout: Layout, a: &[f32], b: &[f32], m: usize, n: usize, k: usize) -> Reference {
let a_at = |i: usize, p: usize| -> f64 {
match layout {
Layout::Nn | Layout::Nt => a[i * k + p] as f64,
Layout::Tn => a[p * m + i] as f64,
}
};
let b_at = |p: usize, j: usize| -> f64 {
match layout {
Layout::Nn | Layout::Tn => b[p * n + j] as f64,
Layout::Nt => b[j * k + p] as f64,
}
};
let mut c = vec![0.0f64; m * n];
let mut mag = vec![0.0f64; m * n];
for i in 0..m {
for j in 0..n {
let (mut acc, mut abs) = (0.0f64, 0.0f64);
for p in 0..k {
let term = a_at(i, p) * b_at(p, j);
acc += term;
abs += term.abs();
}
c[i * n + j] = acc;
mag[i * n + j] = abs;
}
}
Reference { c, mag }
}
pub const U_F32: f64 = 5.960_464_477_539_063e-8;
pub const U_BF16: f64 = 3.906_25e-3;
fn gamma(n: usize, u: f64) -> f64 {
let nu = n as f64 * u;
assert!(nu < 1.0, "error bound degenerate at K={n}; shrink K");
nu / (1.0 - nu)
}
pub fn tolerance(k: usize, mag: f64, operand_u: f64) -> f64 {
(gamma(k + 8, U_F32) + 2.0 * operand_u) * mag
}
pub fn assert_within_bound(label: &str, got: &[f32], r: &Reference, k: usize, operand_u: f64) {
assert_eq!(got.len(), r.c.len(), "{label}: result length mismatch");
let mut worst = 0.0f64;
let mut worst_at = 0usize;
for (idx, (&g, (&want, &mag))) in got.iter().zip(r.c.iter().zip(r.mag.iter())).enumerate() {
assert!(g.is_finite(), "{label}: non-finite output at [{idx}] = {g}");
let tol = tolerance(k, mag, operand_u);
let err = (g as f64 - want).abs();
let scaled = if tol > 0.0 {
err / tol
} else if err == 0.0 {
0.0
} else {
f64::INFINITY
};
if scaled > worst {
worst = scaled;
worst_at = idx;
}
}
assert!(
worst <= 1.0,
"{label}: element [{worst_at}] is {worst:.3}x its error budget \
(got {}, want {}, |a.b| sum {}, tol {:.3e})",
got[worst_at],
r.c[worst_at],
r.mag[worst_at],
tolerance(k, r.mag[worst_at], operand_u),
);
}
pub fn buf(rt: &Arc<GpuRuntime>, data: &[f32]) -> GpuBuffer {
let b = rt.alloc_buffer(data.len().max(1) * 4).expect("alloc");
b.write_f32(data);
b
}
pub fn empty(rt: &Arc<GpuRuntime>, elems: usize) -> GpuBuffer {
let b = rt.alloc_buffer(elems.max(1) * 4).expect("alloc");
b.zero();
b
}
pub fn buf_bf16(rt: &Arc<GpuRuntime>, data: &[f32]) -> GpuBuffer {
let b = rt.alloc_buffer(data.len().max(1) * 2).expect("alloc");
b.write_bf16_bits(&f32_slice_to_bf16(data));
b
}
pub fn seeded(rt: &Arc<GpuRuntime>, elems: usize, value: f32) -> GpuBuffer {
buf(rt, &vec![value; elems.max(1)])
}
pub fn pack_nibbles(nibbles: &[u8]) -> Vec<u8> {
nibbles
.chunks(2)
.map(|p| (p[0] & 0x0f) | ((p.get(1).copied().unwrap_or(0) & 0x0f) << 4))
.collect()
}
pub fn q4_mlx_matrix(rows: usize, cols: usize, group: usize) -> (Vec<u8>, Vec<f32>, Vec<f32>) {
let nibbles: Vec<u8> = (0..rows * cols).map(|i| ((i * 5) % 16) as u8).collect();
let groups = rows * (cols / group);
let sb_f32: Vec<f32> = (0..groups * 2)
.map(|i| {
if i % 2 == 0 {
0.03 + (i % 7) as f32 * 0.002
} else {
(i % 5) as f32 * 0.1 - 0.2
}
})
.collect();
let sb_round = round_trip_bf16(&sb_f32);
let mut dense = vec![0.0f32; rows * cols];
for r in 0..rows {
for c in 0..cols {
let gi = r * (cols / group) + c / group;
dense[r * cols + c] =
sb_round[gi * 2] * nibbles[r * cols + c] as f32 + sb_round[gi * 2 + 1];
}
}
(pack_nibbles(&nibbles), sb_f32, dense)
}
pub fn dense_gemv(dense: &[f32], x: &[f32], rows: usize, cols: usize) -> Vec<f32> {
(0..rows)
.map(|r| {
(0..cols)
.map(|c| dense[r * cols + c] as f64 * x[c] as f64)
.sum::<f64>() as f32
})
.collect()
}
pub fn close_rel(what: &str, got: &[f32], want: &[f32], rel: f32) {
assert_eq!(got.len(), want.len(), "{what}: length");
for (i, (g, w)) in got.iter().zip(want).enumerate() {
assert!(g.is_finite(), "{what}[{i}]: non-finite {g}");
let tol = rel * w.abs().max(1.0);
assert!((g - w).abs() <= tol, "{what}[{i}]: got {g} want {w}");
}
}