mod common;
use common::{Rng, random_layout};
use nuvec_core::*;
type F = fn(&[Array]) -> Result<Array>;
fn take(a: &Array, axis: usize, i: usize) -> Array {
let mut args = vec![SliceArg::full(); a.ndim()];
args[axis] = SliceArg::new(Some(i as isize), Some(i as isize + 1), 1);
squeeze(&slice(a, &args).unwrap(), &[axis as isize]).unwrap()
}
fn assert_close(name: &str, got: &Array, expected: &Array) {
assert_eq!(got.shape(), expected.shape(), "{name}: shape");
assert_eq!(got.dtype(), expected.dtype(), "{name}: dtype");
let g = got
.astype(DType::F64)
.and_then(|g| g.to_vec::<f64>())
.unwrap_or_else(|e| panic!("{name}: {e}"));
let e = expected
.astype(DType::F64)
.unwrap()
.to_vec::<f64>()
.unwrap();
for (i, (g, e)) in g.iter().zip(&e).enumerate() {
assert!(
g == e || (g - e).abs() <= 1e-12 * e.abs().max(1.0) || (g.is_nan() && e.is_nan()),
"{name}: element {i}: {g} vs per-example {e}"
);
}
}
fn check(name: &str, f: F, shapes: &[&[usize]], shared: &[bool]) {
let mut rng = Rng::new(name.bytes().map(u64::from).sum());
let b = 3;
for round in 0..4 {
let axes: Vec<Option<usize>> = shapes
.iter()
.enumerate()
.map(|(k, s)| {
(!shared.get(k).copied().unwrap_or(false)).then(|| rng.below(s.len() + 1))
})
.collect();
let args: Vec<Array> = shapes
.iter()
.zip(&axes)
.map(|(s, ax)| {
let mut shape = s.to_vec();
if let Some(ax) = ax {
shape.insert(*ax, b);
}
let n = shape.iter().product();
let values: Vec<f64> = (0..n).map(|_| (rng.f64() * 0.2).round() * 0.5).collect();
random_layout(&mut rng, &shape, values).0
})
.collect();
let expected: Vec<Array> = (0..b)
.map(|i| {
let xs: Vec<Array> = args
.iter()
.zip(&axes)
.map(|(a, ax)| ax.map_or_else(|| a.clone(), |ax| take(a, ax, i)))
.collect();
f(&xs).unwrap()
})
.collect();
let out_axis = rng.below(expected[0].ndim() + 1);
let in_axes: Vec<Option<isize>> = axes.iter().map(|a| a.map(|a| a as isize)).collect();
let got = vmap(|xs| Ok(vec![f(xs)?]), &in_axes, &[out_axis as isize])(&args)
.unwrap_or_else(|e| panic!("{name} round {round} {axes:?}: {e}"))
.remove(0);
for (i, e) in expected.iter().enumerate() {
assert_close(
&format!("{name} round {round} axes {axes:?} out {out_axis} example {i}"),
&take(&got, out_axis, i),
e,
);
}
}
}
#[test]
#[cfg_attr(
miri,
ignore = "hundreds of evaluations: too slow under Miri, and covered natively"
)]
fn elementwise() {
let unary: [(&str, F); 9] = [
("negative", |x| negative(&x[0])),
("abs", |x| abs(&x[0])),
("exp", |x| exp(&x[0])),
("log", |x| log(&abs(&x[0])?)),
("sqrt", |x| sqrt(&abs(&x[0])?)),
("sin", |x| sin(&x[0])),
("cos", |x| cos(&x[0])),
("tanh", |x| tanh(&x[0])),
("sigmoid", |x| sigmoid(&x[0])),
];
for (name, f) in unary {
check(name, f, &[&[2, 3]], &[]);
}
let binary: [(&str, F); 8] = [
("add", |x| add(&x[0], &x[1])),
("subtract", |x| subtract(&x[0], &x[1])),
("multiply", |x| multiply(&x[0], &x[1])),
("divide", |x| divide(&x[0], &add(&abs(&x[1])?, 1.0)?)),
("maximum", |x| maximum(&x[0], &x[1])),
("minimum", |x| minimum(&x[0], &x[1])),
("less", |x| less(&x[0], &x[1])),
("equal", |x| equal(&x[0], &x[1])),
];
for (name, f) in binary {
check(name, f, &[&[2, 3], &[2, 3]], &[]);
check(&format!("{name}-broadcast"), f, &[&[2, 3], &[3]], &[]);
check(
&format!("{name}-shared"),
f,
&[&[3, 1], &[4]],
&[false, true],
);
check(&format!("{name}-scalar"), f, &[&[], &[2]], &[]);
}
check(
"where",
|x| where_(greater(&x[0], 0.0)?, &x[1], &x[2]),
&[&[2, 3], &[3], &[2, 1]],
&[],
);
check(
"where-shared-cond",
|x| where_(greater(&x[0], 0.0)?, &x[1], 0.0),
&[&[3], &[3]],
&[true],
);
check("astype", |x| x[0].astype(DType::F32), &[&[2, 3]], &[]);
check("stop_gradient", |x| stop_gradient(&x[0]), &[&[2, 3]], &[]);
check(
"scalar-operand",
|x| subtract(1.0, multiply(&x[0], 2)?),
&[&[4]],
&[],
);
}
#[test]
#[cfg_attr(
miri,
ignore = "hundreds of evaluations: too slow under Miri, and covered natively"
)]
fn views() {
let cases: [(&str, F, &[usize]); 8] = [
("reshape", |x| x[0].reshape(&[3, 2]), &[2, 3]),
("reshape-infer", |x| x[0].reshape(&[-1]), &[2, 3, 2]),
("transpose", |x| transpose(&x[0], &[2, 0, 1]), &[2, 3, 4]),
("t", |x| x[0].t(), &[2, 3]),
(
"slice",
|x| {
slice(
&x[0],
&[
SliceArg::new(Some(1), None, 1),
SliceArg::new(None, None, -2),
],
)
},
&[3, 5],
),
("broadcast_to", |x| broadcast_to(&x[0], &[2, 3, 4]), &[3, 1]),
("expand_dims", |x| expand_dims(&x[0], -1), &[2, 3]),
("squeeze", |x| squeeze(&x[0], &[]), &[1, 3, 1]),
];
for (name, f, shape) in cases {
check(name, f, &[shape], &[]);
}
}
#[test]
#[cfg_attr(
miri,
ignore = "hundreds of evaluations: too slow under Miri, and covered natively"
)]
fn reductions_and_matmul() {
let cases: [(&str, F); 9] = [
("sum", |x| sum(&x[0], &[1], false)),
("sum-all", |x| sum(&x[0], &[], false)),
("prod", |x| prod(&x[0], &[0, 2], true)),
("max", |x| max(&x[0], &[-1], false)),
("min", |x| min(&x[0], &[], true)),
("mean", |x| mean(&x[0], &[0], false)),
("all", |x| all(&greater(&x[0], 0.0)?, &[1], false)),
("argmax", |x| argmax(&x[0], Some(1), false)),
("argmin-flat", |x| argmin(&x[0], None, false)),
];
for (name, f) in cases {
check(name, f, &[&[2, 3, 4]], &[]);
}
let mm: F = |x| matmul(&x[0], &x[1]);
check("matmul", mm, &[&[3, 4], &[4, 2]], &[]);
check(
"matmul-shared-left",
mm,
&[&[3, 4], &[4, 2]],
&[true, false],
);
check(
"matmul-shared-right",
mm,
&[&[3, 4], &[4, 2]],
&[false, true],
);
check("matmul-batch", mm, &[&[2, 3, 4], &[4, 2]], &[]);
check("matmul-vector", mm, &[&[4], &[4, 3]], &[]);
}
#[test]
fn composition_with_differentiation() {
let f = |x: &Array| sum(&multiply(&sin(x)?, x)?, &[], false);
let xs = Array::from_slice(&[0.1f64, 0.2, 0.3, 0.4, 0.5, 0.6], &[3, 2]).unwrap();
let per_example =
vmap(move |xs| Ok(vec![grad(f)(&xs[0])?]), &[], &[])(std::slice::from_ref(&xs))
.unwrap()
.remove(0);
for i in 0..3 {
assert_close(
"vmap(grad)",
&take(&per_example, 0, i),
&grad(f)(&take(&xs, 0, i)).unwrap(),
);
}
let batched_sum = grad(move |xs: &Array| {
let ys = vmap(move |v| Ok(vec![f(&v[0])?]), &[], &[])(std::slice::from_ref(xs))?;
sum(&ys[0], &[], false)
});
assert_close("grad(vmap)", &batched_sum(&xs).unwrap(), &per_example);
let t = Array::from_slice(&[1.0f64, 0.0, 0.0, 1.0, 1.0, 1.0], &[3, 2]).unwrap();
let (_, jt) = jvp(
|v| vmap(|x| Ok(vec![sin(&x[0])?]), &[], &[])(v),
std::slice::from_ref(&xs),
std::slice::from_ref(&t),
)
.unwrap();
assert_close(
"jvp(vmap)",
&jt[0],
&multiply(cos(&xs).unwrap(), &t).unwrap(),
);
let g = vmap(
|v| {
Ok(vec![grad(|x: &Array| {
sum(&slice(x, &[SliceArg::new(None, None, -2)])?, &[], false)
})(&v[0])?])
},
&[],
&[],
)(&[Array::from_slice(&[1.0f32; 6], &[2, 3]).unwrap()])
.unwrap();
assert_eq!(
g[0].to_vec::<f32>().unwrap(),
[1.0, 0.0, 1.0, 1.0, 0.0, 1.0]
);
}
#[test]
fn nested_vmap() {
let outer = vmap(
|xs| {
vmap(
|ys| Ok(vec![multiply(&ys[0], &ys[1])?]),
&[Some(0), None],
&[],
)(xs)
},
&[Some(1), Some(0)],
&[],
);
let a = Array::from_slice(&[1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], &[3, 2]).unwrap(); let b = Array::from_slice(&[10.0f32, 100.0], &[2]).unwrap();
let out = outer(&[a, b]).unwrap().remove(0);
assert_eq!(out.shape(), &[2, 3]);
assert_eq!(
out.to_vec::<f32>().unwrap(),
[10.0, 30.0, 50.0, 200.0, 400.0, 600.0]
);
}
#[test]
fn mlx_edge_cases_and_errors() {
let x = Array::from_slice(&[1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3]).unwrap();
let c = vmap(|_| Ok(vec![Array::scalar(1.0f32)]), &[], &[])(std::slice::from_ref(&x)).unwrap();
assert_eq!(c[0].to_vec::<f32>().unwrap(), [1.0, 1.0]);
let s = vmap(|v| Ok(vec![sum(&v[0], &[], false)?]), &[Some(-1)], &[])(std::slice::from_ref(&x));
assert_eq!(s.unwrap()[0].to_vec::<f32>().unwrap(), [5.0, 7.0, 9.0]);
let t = vmap(|v| Ok(vec![&v[0] * 2]), &[], &[1])(std::slice::from_ref(&x)).unwrap();
assert_eq!(t[0].shape(), &[3, 2]);
let empty = Array::from_slice::<f32>(&[], &[0, 3]).unwrap();
let e = vmap(|v| Ok(vec![&v[0] * 2]), &[], &[])(&[empty]).unwrap();
assert_eq!(e[0].shape(), &[0, 3]);
let add2 = |v: &[Array]| Ok(vec![add(&v[0], &v[1])?]);
let ones3 = Array::from_slice(&[1.0f32; 9], &[3, 3]).unwrap();
let invalid = |r: Result<Vec<Array>>| matches!(r, Err(Error::InvalidArgument(_)));
assert!(invalid(vmap(add2, &[None, None], &[])(&[
x.clone(),
x.clone()
])));
assert!(invalid(vmap(add2, &[], &[])(&[x.clone(), ones3])));
assert!(invalid(vmap(add2, &[Some(2), None], &[])(&[
x.clone(),
x.clone()
])));
assert!(invalid(vmap(add2, &[Some(0)], &[])(&[
x.clone(),
x.clone()
])));
assert!(invalid(vmap(add2, &[], &[0, 0])(&[x.clone(), x.clone()])));
let evaluates = |v: &[Array]| {
let y = &v[0] * 2;
y.eval()?;
Ok(vec![y])
};
let err = vmap(evaluates, &[], &[])(std::slice::from_ref(&x)).unwrap_err();
assert!(err.to_string().contains("inside vmap"), "{err}");
let failing =
|_: &[Array]| -> Result<Vec<Array>> { Err(Error::InvalidArgument("user".into())) };
assert!(matches!(vmap(failing, &[], &[])(&[x]), Err(Error::InvalidArgument(m)) if m == "user"));
}
#[cfg(target_os = "macos")]
#[test]
fn vmap_on_the_gpu_matches_the_cpu() {
if cfg!(miri) {
return;
}
let run = |device: Device| {
let previous = default_stream();
set_default_stream(Stream::default_for(device));
let xs = arange(0.0, 24.0, 1.0, DType::F32)
.unwrap()
.reshape(&[2, 3, 4])
.unwrap();
let w = arange(0.0, 8.0, 0.5, DType::F32)
.unwrap()
.reshape(&[4, 4])
.unwrap();
let f = vmap(
|v| Ok(vec![sum(&tanh(&matmul(&v[0], &v[1])?)?, &[0], false)?]),
&[Some(0), None],
&[],
);
let r = f(&[xs, w]).and_then(|o| o[0].to_vec::<f32>());
set_default_stream(previous);
r
};
let cpu = run(Device::cpu()).unwrap();
match run(Device::gpu(0)) {
Ok(gpu) => {
for (c, g) in cpu.iter().zip(&gpu) {
assert!(
(c - g).abs() <= 1e-5 * c.abs().max(1.0),
"cpu {c} vs gpu {g}"
);
}
}
Err(e) => eprintln!("skipped: no Metal device ({e})"),
}
}