#![cfg(target_os = "macos")]
mod common;
use nuvec_core::ops;
use nuvec_core::*;
fn gpu() -> bool {
let ok = common::require_gpu().is_some();
set_default_stream(Stream::default_for(Device::gpu(0)));
ok
}
fn normal(shape: &[usize], dtype: DType, seed: u64) -> Array {
let n: usize = shape.iter().product();
let t = ops::arange(0.0, n as f64, 1.0);
let x = (&t * (0.7381 + seed as f64 * 0.0137)).sin();
let x = &x * (&t * (0.0071 * seed as f64 + 0.013)).cos();
x.reshape(shape).astype(dtype)
}
fn workload() -> Result<Vec<Array>> {
let (f32_, bf16_) = (DType::F32, DType::BF16);
let mut out = Vec::new();
out.push(normal(&[32, 16384], f32_, 1).matmul(&normal(&[16384, 32], f32_, 2)));
out.push(normal(&[512, 1024], bf16_, 3).matmul(&normal(&[1024, 768], bf16_, 4)));
out.push(normal(&[2048, 256], bf16_, 34).matmul(&normal(&[256, 2048], bf16_, 35)));
let w = normal(&[4096, 4096], bf16_, 5);
out.push(normal(&[1, 4096], bf16_, 6).matmul(&w.t()));
let (wq, scales, biases) = w.quantize(64, 4);
for (rows, seed) in [(1, 7), (8, 8), (256, 9)] {
let x = normal(&[rows, 4096], bf16_, seed);
out.push(x.quantized_matmul(&wq, &scales, &biases, true, 64, 4));
}
out.push(normal(&[1, 4096], bf16_, 10).quantized_matmul(&wq, &scales, &biases, false, 64, 4));
let experts = normal(&[16, 512, 1024], bf16_, 11);
let (eq, es, eb) = experts.quantize(64, 4);
let ids: Vec<u32> = (0..512u32).map(|i| i * 16 / 512).collect();
let ids = Array::from(ids);
out.push(normal(&[512, 1, 1024], bf16_, 12).gather_qmm(
&eq,
&es,
&eb,
None,
Some(&ids),
true,
64,
4,
true,
));
let (k, v) = (
normal(&[1, 4, 8192, 128], bf16_, 13),
normal(&[1, 4, 8192, 128], bf16_, 14),
);
out.push(fast::scaled_dot_product_attention(
&normal(&[1, 16, 1, 128], bf16_, 15),
&k,
&v,
0.088,
&fast::AttentionOptions::default(),
));
let (k, v) = (
normal(&[1, 4, 300, 128], bf16_, 16),
normal(&[1, 4, 300, 128], bf16_, 17),
);
out.push(fast::scaled_dot_product_attention(
&normal(&[1, 16, 300, 128], bf16_, 18),
&k,
&v,
0.088,
&fast::AttentionOptions::causal(),
));
out.push(normal(&[1 << 22], f32_, 19).sum(..));
let rows = normal(&[64, 32768], bf16_, 20);
out.push(rows.logsumexp(-1));
out.push(rows.softmax(-1));
out.push(rows.max(0));
out.push(rows.sum(0));
let long = normal(&[4, 50000], f32_, 21);
out.push(long.sort(-1));
out.push(long.argsort(-1));
out.push(normal(&[4, 100000], f32_, 22).cumsum(-1));
let (h, n) = fast::add_rms_norm(
&normal(&[64, 4096], bf16_, 23),
&normal(&[64, 4096], bf16_, 24),
Some(&normal(&[4096], bf16_, 25)),
1e-6,
);
out.extend([h, n]);
let rotation = fast::Rotation {
dims: 128,
traditional: false,
base: Some(1e6),
scale: 1.0,
freqs: None,
};
out.push(fast::norm_rope(
&normal(&[1, 64, 8, 128], bf16_, 26).transpose([0, 2, 1, 3]),
Some((&normal(&[128], bf16_, 27), 1e-6)),
rotation,
40,
Some(&normal(&[1, 8, 256, 128], bf16_, 28)),
));
let (y, state) = fast::gated_delta(
&(normal(&[1, 64, 4, 128], bf16_, 29) * 0.05),
&(normal(&[1, 64, 4, 128], bf16_, 30) * 0.05),
&normal(&[1, 64, 8, 128], bf16_, 31),
&normal(&[1, 64, 8], f32_, 32).sigmoid(),
&normal(&[1, 64, 8], bf16_, 33).sigmoid(),
&ops::zeros([1, 8, 128, 128], f32_),
);
out.extend([y, state]);
Ok(out)
}
fn bits(a: &Array) -> Vec<u64> {
match a.dtype().category() {
Category::Float => a
.astype(DType::F32)
.to_vec::<f32>()
.unwrap()
.into_iter()
.map(|v| u64::from(v.to_bits()))
.collect(),
_ => a
.astype(DType::I64)
.to_vec::<i64>()
.unwrap()
.into_iter()
.map(|v| v as u64)
.collect(),
}
}
#[test]
fn results_are_bit_identical_across_runs() {
if !gpu() {
return;
}
let first: Vec<Vec<u64>> = workload().unwrap().iter().map(bits).collect();
let mut pending: Option<Vec<Array>> = None;
for run in 0..5 {
let noise: Vec<Array> = (0..(run + 1) * 3)
.map(|i| normal(&[1000 + 997 * i, 64], DType::F32, 100 + i as u64) * 2.0)
.collect();
async_eval(&noise).unwrap();
let outputs = workload().unwrap();
async_eval(&outputs).unwrap();
drop(noise);
if let Some(previous) = pending.replace(outputs) {
check(&first, &previous, run);
}
}
check(&first, &pending.unwrap(), 5);
}
fn check(first: &[Vec<u64>], outputs: &[Array], run: usize) {
assert_eq!(first.len(), outputs.len());
for (i, (want, got)) in first.iter().zip(outputs).enumerate() {
let got = bits(got);
let differ = want.iter().zip(&got).filter(|(a, b)| a != b).count();
assert_eq!(
differ, 0,
"run {run}: output {i} differs in {differ} elements"
);
}
}
fn draw() -> Vec<f32> {
set_default_stream(Stream::default_for(Device::cpu()));
random::uniform(0.0, 1.0, [8], DType::F32, None)
.to_vec::<f32>()
.unwrap()
}
#[test]
fn threads_draw_distinct_reproducible_streams() {
let (a, b) = (std::thread::spawn(draw), std::thread::spawn(draw));
assert_ne!(a.join().unwrap(), b.join().unwrap(), "unseeded threads");
let run = || {
random::seed(7);
let first = std::thread::spawn(draw).join().unwrap();
(first, std::thread::spawn(draw).join().unwrap())
};
let (first, second) = run();
assert_ne!(first, second, "workers of one run");
assert_eq!(run(), (first, second), "the same in every run");
}
fn hash(outputs: &[Array]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
let mut eat = |v: u64| {
for byte in v.to_le_bytes() {
h ^= u64::from(byte);
h = h.wrapping_mul(0x0100_0000_01b3);
}
};
for out in outputs {
for &d in out.shape() {
eat(d as u64);
}
for v in bits(out) {
eat(v);
}
}
h
}
#[test]
#[ignore = "run by results_are_bit_identical_across_processes in a child process"]
fn child_prints_the_workload_hash() {
if std::env::var_os("NUVEC_DETERMINISM_CHILD").is_none() || !gpu() {
return;
}
println!("\nhash={:016x}", hash(&workload().unwrap()));
}
#[test]
fn results_are_bit_identical_across_processes() {
if !gpu() {
return;
}
let child = || {
let out = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"child_prints_the_workload_hash",
"--ignored",
"--nocapture",
"--test-threads=1",
])
.env("NUVEC_DETERMINISM_CHILD", "1")
.output()
.unwrap();
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
stdout
.lines()
.find_map(|l| l.strip_prefix("hash="))
.expect("a hash line")
.to_string()
};
let (a, b) = (child(), child());
assert_eq!(a, b, "two processes differ");
let own = format!("{:016x}", hash(&workload().unwrap()));
assert_eq!(a, own, "a child differs from this process");
let config = match std::env::var("NUVEC_METALLIB").as_deref() {
Ok("off") => "runtime",
_ => "precompiled",
};
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/determinism");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(format!("{config}.hash")), &own).unwrap();
}
#[test]
fn float_atomics_are_documented() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let docs = std::fs::read_to_string(root.join("crates/nuvec/src/lib.rs")).unwrap();
let section = docs
.split("# Determinism")
.nth(1)
.expect("a Determinism section in the nuvec crate docs");
let listed = section
.split("Not deterministic")
.nth(1)
.expect("a list of non-deterministic operations");
let dir = root.join("crates/nuvec-kernels/src/metal");
let mut found = Vec::new();
for entry in std::fs::read_dir(&dir).unwrap() {
let path = entry.unwrap().path();
let name = path.file_name().unwrap().to_string_lossy().into_owned();
if !(name.ends_with(".metal") || name.ends_with(".h")) {
continue;
}
let source = std::fs::read_to_string(&path).unwrap();
let float_atomics = source.contains("atomic_float") || source.contains("atomic_add_16");
if float_atomics {
found.push(name.clone());
assert!(
listed.contains(&format!("`{name}`")),
"{name} uses float atomics but is not in the Determinism section's list"
);
}
}
assert!(found.contains(&"indexing.metal".to_string()), "{found:?}");
}
#[test]
fn unique_index_scatters_are_deterministic() {
if !gpu() {
return;
}
let base = normal(&[64, 4096], DType::F32, 40);
let values = normal(&[64, 16], DType::F32, 41);
let idx: Vec<i32> = (0..64 * 16)
.map(|i| ((i % 16) * 251 + i / 16) % 4096)
.collect();
let idx = Array::from_slice(&idx, [64, 16]);
let run = || {
let put = base.put_along_axis(&idx, &values, -1);
let fill = base.put_along_axis(&idx, -1e9f32, -1);
let add = base.scatter_add_along_axis(&idx, &values, -1);
vec![bits(&put), bits(&fill), bits(&add)]
};
let first = run();
for i in 0..20 {
assert_eq!(run(), first, "run {i}");
}
}