#![cfg(target_os = "macos")]
mod common;
use common::Rng;
use nuvec_core::*;
fn nax_active() -> bool {
if common::require_gpu().is_none() {
return false;
}
let Some(device) = nuvec_gpu::Device::shared() else {
return false;
};
set_default_stream(Stream::default_for(Device::gpu(0)));
let status = device.nax();
eprintln!("NAX on {}: {status:?}", device.info().architecture);
status.is_active()
}
fn random(rng: &mut Rng, shape: &[usize], dtype: DType) -> Array {
let n: usize = shape.iter().product();
let v: Vec<f32> = (0..n)
.map(|_| rng.below(2001) as f32 / 1000.0 - 1.0)
.collect();
Array::from_slice(&v, shape).astype(dtype)
}
fn scaled_error(got: &Array, want: &Array, k: usize) -> f64 {
let g = got.astype(DType::F64).to_vec::<f64>().unwrap();
let w = want.astype(DType::F64).to_vec::<f64>().unwrap();
g.iter()
.zip(&w)
.map(|(a, b)| (a - b).abs() / ((k as f64).sqrt() + b.abs()))
.fold(0.0, f64::max)
}
fn tolerance(dtype: DType) -> f64 {
match dtype {
DType::F16 => 4e-3,
DType::BF16 => 3e-2,
_ => 4e-3,
}
}
#[test]
fn the_nax_status_is_reported() {
if common::require_gpu().is_none() {
return;
}
let Some(device) = nuvec_gpu::Device::shared() else {
return;
};
let status = device.nax();
eprintln!("NAX: {status:?}");
if std::env::var("NUVEC_NAX").is_err() {
assert!(!status.is_active(), "{status:?}");
}
}
#[test]
fn nax_products_equal_the_cpu_and_the_simd_kernels() {
if !nax_active() {
return;
}
let mut rng = Rng::new(7);
let shapes = [
(64, 64, 64),
(9, 17, 33),
(100, 37, 70),
(513, 1000, 333),
(128, 256, 4096),
(2048, 512, 1024),
];
for (m, n, k) in shapes {
for dtype in [DType::F16, DType::BF16, DType::F32] {
for (ta, tb) in [(false, false), (false, true), (true, false), (true, true)] {
let a = match ta {
true => random(&mut rng, &[k, m], dtype).t(),
false => random(&mut rng, &[m, k], dtype),
};
let b = match tb {
true => random(&mut rng, &[n, k], dtype).t(),
false => random(&mut rng, &[k, n], dtype),
};
let name = format!("{m}x{n}x{k} {} ta {ta} tb {tb}", dtype.name());
let got = a.matmul(&b);
let cpu = with_stream(Device::cpu(), || {
a.astype(DType::F32).matmul(&b.astype(DType::F32))
});
let e = scaled_error(&got, &cpu, k);
assert!(e < tolerance(dtype), "{name}: {e}");
let previous = __nax_route(false);
let simd = a.matmul(&b);
simd.eval().unwrap();
__nax_route(previous);
let e = scaled_error(&got, &simd, k);
assert!(e < tolerance(dtype) * 2.0, "{name} vs SIMD: {e}");
}
}
}
}
#[test]
fn nax_products_batch_and_take_the_addmm_epilogue() {
if !nax_active() {
return;
}
let mut rng = Rng::new(11);
for dtype in [DType::F16, DType::F32] {
let a = random(&mut rng, &[3, 2, 70, 96], dtype);
let b = random(&mut rng, &[2, 96, 50], dtype);
let got = a.matmul(&b);
let cpu = with_stream(Device::cpu(), || {
a.astype(DType::F32).matmul(&b.astype(DType::F32))
});
assert!(
scaled_error(&got, &cpu, 96) < tolerance(dtype),
"{}",
dtype.name()
);
let x = random(&mut rng, &[130, 64], dtype);
let w = random(&mut rng, &[64, 90], dtype);
let c = random(&mut rng, &[90], dtype);
for (alpha, beta) in [(1.0, 1.0), (0.5, 2.0), (1.0, 0.0)] {
let got = c.addmm(&x, &w, alpha, beta);
let cpu = with_stream(Device::cpu(), || {
c.astype(DType::F32).addmm(
&x.astype(DType::F32),
&w.astype(DType::F32),
alpha,
beta,
)
});
let e = scaled_error(&got, &cpu, 64);
assert!(
e < tolerance(dtype),
"{} addmm {alpha} {beta}: {e}",
dtype.name()
);
}
}
}
#[test]
fn nax_quantized_products_equal_the_cpu_and_the_simd_kernels() {
if !nax_active() {
return;
}
let mut rng = Rng::new(13);
for (m, n, k) in [
(64, 64, 64),
(100, 200, 256),
(512, 640, 1024),
(1000, 1100, 256),
] {
for bits in [2i32, 3, 4, 8] {
for gs in [32i32, 64, 128].into_iter().filter(|&g| g as usize <= k) {
for dtype in [DType::F16, DType::BF16] {
let w = random(&mut rng, &[n, k], dtype);
let q = w.quantize(Quant::affine(gs, bits));
let x = random(&mut rng, &[m, k], dtype);
let name = format!("{m}x{n}x{k} {} gs {gs} b {bits}", dtype.name());
let got = x.quantized_matmul(&q, true);
let deq = q.dequantize(None);
let cpu = with_stream(Device::cpu(), || {
x.astype(DType::F32).matmul(&deq.astype(DType::F32).t())
});
let e = scaled_error(&got, &cpu, k);
assert!(e < tolerance(dtype) * 2.0, "{name}: {e}");
let previous = __nax_route(false);
let simd = x.quantized_matmul(&q, true);
simd.eval().unwrap();
__nax_route(previous);
let e = scaled_error(&got, &simd, k);
assert!(e < tolerance(dtype) * 2.0, "{name} vs SIMD: {e}");
}
}
}
}
}
#[test]
fn nax_sorted_gathers_equal_the_cpu_and_the_simd_kernels() {
if !nax_active() {
return;
}
let mut rng = Rng::new(23);
let (experts, n, k) = (8, 192, 256);
let runs = [0usize, 1, 63, 64, 65, 130, 0, 7];
let ids: Vec<u32> = runs
.iter()
.enumerate()
.flat_map(|(e, &r)| std::iter::repeat_n(e as u32, r))
.collect();
let rows = ids.len();
let ids = Array::from(ids);
for bits in [2i32, 4, 8] {
for dtype in [DType::F16, DType::BF16] {
let w = random(&mut rng, &[experts, n, k], dtype);
let q = w.quantize(Quant::affine(64, bits));
let x = random(&mut rng, &[rows, 1, k], dtype);
let gather = |x: &Array| {
x.gather_qmm(
&q,
None,
Some(&ids),
GatherQmm::default().sorted_indices(true),
)
};
let got = gather(&x);
let name = format!("{} b {bits}", dtype.name());
let deq = q.dequantize(None).astype(DType::F32);
let cpu = with_stream(Device::cpu(), || {
let picked = deq.take(&ids, 0); x.astype(DType::F32).matmul(&picked.transpose([0, 2, 1]))
});
let e = scaled_error(&got, &cpu, k);
assert!(e < tolerance(dtype) * 2.0, "{name}: {e}");
let previous = __nax_route(false);
let simd = gather(&x);
simd.eval().unwrap();
__nax_route(previous);
let e = scaled_error(&got, &simd, k);
assert!(e < tolerance(dtype) * 2.0, "{name} vs SIMD: {e}");
}
}
}
#[test]
fn the_nax_kernels_are_the_ones_that_run() {
if !nax_active() || !nuvec_gpu::profile::enabled() {
return;
}
let mut rng = Rng::new(17);
nuvec_gpu::profile::reset();
let a = random(&mut rng, &[1024, 256], DType::F16);
let b = random(&mut rng, &[256, 1024], DType::F16);
a.matmul(&b).eval().unwrap();
let q = random(&mut rng, &[1024, 256], DType::F16).quantize(Quant::affine(64, 4));
a.quantized_matmul(&q, true).eval().unwrap();
random(&mut rng, &[24, 256], DType::F16)
.matmul(&b)
.eval()
.unwrap();
let long = random(&mut rng, &[96, 4096], DType::F16);
long.matmul(&random(&mut rng, &[4096, 96], DType::F16))
.eval()
.unwrap();
let experts = random(&mut rng, &[4, 128, 256], DType::F16).quantize(Quant::affine(64, 4));
let ids = Array::from((0..256u32).map(|i| i / 64).collect::<Vec<_>>());
random(&mut rng, &[256, 1, 256], DType::F16)
.gather_qmm(
&experts,
None,
Some(&ids),
GatherQmm::default().sorted_indices(true),
)
.eval()
.unwrap();
let names: Vec<String> = nuvec_gpu::profile::report()
.into_iter()
.map(|r| r.0)
.collect();
for kernel in [
"nuvec_nax_gemm_nn_float16_bm64_bn64",
"nuvec_nax_gemm_nn_float16_bm32_bn64",
"nuvec_nax_gemm_splitk_nn_float16_bm64_bn64",
"nuvec_gemm_splitk_accum_float16",
"nuvec_nax_gather_qmm_rhs_float16_gs64_b4",
] {
assert!(
names.iter().any(|n| n.starts_with(kernel)),
"{kernel}: {names:?}"
);
}
assert!(
names
.iter()
.any(|n| n.starts_with("nuvec_nax_qmm_t_float16_gs64_b4")),
"{names:?}"
);
}
type Product = Box<dyn Fn(&[Array]) -> Array>;
fn gradients(
f: impl Fn(&[Array]) -> Array,
primals: &[Array],
cot: &Array,
nax: bool,
) -> Vec<Array> {
let previous = __nax_route(nax);
let (_, grads) = vjp_flat(|xs| Ok(vec![f(xs)]), primals, std::slice::from_ref(cot)).unwrap();
eval(&grads).unwrap();
__nax_route(previous);
grads
}
#[test]
fn nax_derivatives_equal_the_simd_kernels() {
if !nax_active() {
return;
}
let mut rng = Rng::new(17);
let (m, n, k) = (128, 96, 256);
for dtype in [DType::F16, DType::BF16, DType::F32] {
let a = random(&mut rng, &[m, k], dtype);
let b = random(&mut rng, &[k, n], dtype);
let c = random(&mut rng, &[m, n], dtype);
let cot = random(&mut rng, &[m, n], dtype);
let cases: [(&str, Product, Vec<Array>); 2] = [
(
"matmul",
Box::new(|xs| xs[0].matmul(&xs[1])),
vec![a.clone(), b.clone()],
),
(
"addmm",
Box::new(|xs| ops::addmm(&xs[2], &xs[0], &xs[1], 0.5, 2.0)),
vec![a.clone(), b.clone(), c.clone()],
),
];
for (name, f, primals) in cases {
let nax = gradients(&f, &primals, &cot, true);
let simd = gradients(&f, &primals, &cot, false);
for (i, (g, s)) in nax.iter().zip(&simd).enumerate() {
let e = scaled_error(g, s, k.max(m));
assert!(
e < tolerance(dtype) * 2.0,
"{name} {} d{i}: {e}",
dtype.name()
);
}
}
}
for dtype in [DType::F16, DType::BF16] {
let w = random(&mut rng, &[n, k], dtype);
let q = w.quantize(Quant::affine(64, 4));
let x = random(&mut rng, &[m, k], dtype);
let cot = random(&mut rng, &[m, n], dtype);
let (wq, quant) = (q.weight.clone(), q.quant);
let f = |xs: &[Array]| {
let q = Quantized::new(wq.clone(), xs[1].clone(), Some(xs[2].clone()), quant);
xs[0].quantized_matmul(&q, true)
};
let primals = [x, q.scales.clone(), q.biases.clone().unwrap()];
let nax = gradients(f, &primals, &cot, true);
let simd = gradients(f, &primals, &cot, false);
for (i, (g, s)) in nax.iter().zip(&simd).enumerate() {
let e = scaled_error(g, s, k.max(m));
assert!(e < tolerance(dtype) * 2.0, "qmm {} d{i}: {e}", dtype.name());
}
}
}