#![cfg(target_os = "macos")]
use nuvec_compile::compile_flat;
use nuvec_core::{
Array, Device, Result, Stream, checkpoint_flat, eval, ops, set_default_stream,
value_and_grad_flat,
};
fn block(x: &Array, w1: &Array, w2: &Array) -> Result<Array> {
let h = x.matmul(w1).tanh();
let h = h.multiply(&h).tanh();
x.add(h.matmul(w2).tanh()).check()
}
fn peak(checkpointed: bool) -> usize {
let device = nuvec_gpu::Device::shared().expect("checked");
let (b, d) = (4096, 256);
let filled = |shape: &[usize], seed: f64| -> Array {
let n: usize = shape.iter().product();
let t = ops::arange(0.0, n as f64, 1.0);
((&t * seed).sin() * 0.05f32).reshape(shape)
};
let x = filled(&[b, d], 0.37);
let weights: Vec<Array> = (0..8)
.flat_map(|i| {
[
filled(&[d, 4 * d], 0.11 + i as f64),
filled(&[4 * d, d], 0.23 + i as f64),
]
})
.collect();
let mut all = weights.clone();
all.push(x);
eval(&all).unwrap();
type Block = Box<dyn Fn(&[Array]) -> Result<Vec<Array>> + Send + Sync>;
let blocks: Vec<Block> = (0..8)
.map(|_| {
let f = |xs: &[Array]| Ok(vec![block(&xs[0], &xs[1], &xs[2])?]);
if checkpointed {
Box::new(checkpoint_flat(f)) as Box<_>
} else {
Box::new(f) as Box<_>
}
})
.collect();
let loss = move |xs: &[Array]| -> Result<Array> {
let n = xs.len() - 1;
let mut h = xs[n].clone();
for (i, f) in blocks.iter().enumerate() {
h = f(&[h, xs[2 * i].clone(), xs[2 * i + 1].clone()])?.remove(0);
}
h.sum(..).check()
};
let step = compile_flat(move |xs: &[Array]| {
let (value, mut grads) = value_and_grad_flat(&loss, &[16])(xs)?;
grads.push(value);
Ok(grads)
});
let resident = device.memory().active;
device.reset_peak();
let outputs = step(&all).unwrap();
eval(&outputs).unwrap();
let peak = device.memory().peak;
drop(outputs);
peak.saturating_sub(resident)
}
#[test]
fn checkpointed_blocks_lower_peak_memory_under_compile() {
let ok = !cfg!(miri) && {
set_default_stream(Stream::default_for(Device::gpu(0)));
(&Array::from(1.0f32) + 1.0).eval().is_ok()
};
if !ok {
assert!(
std::env::var("NUVEC_REQUIRE_GPU").as_deref() != Ok("1"),
"NUVEC_REQUIRE_GPU=1 but no Metal device is available"
);
eprintln!("skipped: no Metal device");
return;
}
let plain = peak(false);
let checkpointed = peak(true);
assert!(
checkpointed * 10 <= plain * 6,
"checkpointed peak {checkpointed} bytes is not at most 60% of {plain}"
);
}