use std::net::TcpListener;
use std::time::{Duration, Instant};
use nuvec_core::distributed::{self, Group};
use nuvec_core::{Array, DType, Device, Stream, array, grad, set_default_stream, vmap};
mod common;
use common::{CHILD, agree, ranks};
fn values(rank: usize, n: usize) -> Vec<f32> {
(0..n)
.map(|i| ((i * 7 + rank * 13) % 23) as f32 - 11.0)
.collect()
}
fn input(rank: usize, shape: &[usize], dtype: DType) -> Array {
let n = shape.iter().product();
let v: Vec<f32> = match dtype {
DType::Bool => (0..n)
.map(|i| u8::from((i + rank).is_multiple_of(3)) as f32)
.collect(),
d if d.name().starts_with('u') => values(rank, n).iter().map(|x| x + 11.0).collect(),
_ => values(rank, n),
};
let a = Array::from_slice(&v, shape);
if dtype == DType::Bool {
a.not_equal(0.0)
} else {
a.astype(dtype)
}
}
fn as_f64(a: &Array) -> Vec<f64> {
a.astype(DType::F64).to_vec::<f64>().unwrap()
}
fn collectives(g: Group) {
let (rank, size) = (g.rank(), g.size());
let shapes: [&[usize]; 6] = [&[], &[0], &[1], &[3], &[7, 5], &[1001]];
let dtypes = [
DType::Bool,
DType::U8,
DType::U32,
DType::I32,
DType::I64,
DType::F16,
DType::BF16,
DType::F32,
DType::F64,
];
for dtype in dtypes {
for shape in shapes {
let x = input(rank, shape, dtype);
let all: Vec<Vec<f64>> = (0..size).map(|r| as_f64(&input(r, shape, dtype))).collect();
let fold = |f: fn(f64, f64) -> f64| -> Vec<f64> {
(0..all[0].len())
.map(|i| all.iter().map(|v| v[i]).reduce(f).unwrap())
.collect()
};
let name = format!("{} {shape:?}", dtype.name());
let sum = distributed::all_sum(&x, None);
let want_sum: Vec<f64> = match dtype {
DType::Bool => fold(f64::max),
_ => fold(|a, b| a + b),
};
assert_eq!(sum.shape(), x.shape(), "{name}");
assert_eq!(sum.dtype(), dtype, "{name}");
assert_eq!(as_f64(&sum), want_sum, "all_sum {name}");
assert_eq!(
as_f64(&distributed::all_max(&x, None)),
fold(f64::max),
"all_max {name}"
);
assert_eq!(
as_f64(&distributed::all_min(&x, None)),
fold(f64::min),
"all_min {name}"
);
let gathered = distributed::all_gather(&x, None);
let want: Vec<f64> = all.concat();
assert_eq!(as_f64(&gathered), want, "all_gather {name}");
let mut shape_g = shape.to_vec();
match shape_g.first_mut() {
Some(d) => *d *= size,
None => shape_g.push(size),
}
let dims: Vec<i32> = shape_g.iter().map(|&d| d as i32).collect();
assert_eq!(gathered.shape(), dims.as_slice(), "all_gather shape {name}");
}
}
}
#[test]
fn collectives_on_two_ranks() {
ranks("collectives_on_two_ranks", 2, collectives);
}
#[test]
fn collectives_on_three_ranks() {
ranks("collectives_on_three_ranks", 3, collectives);
}
#[test]
fn collectives_on_four_ranks() {
ranks("collectives_on_four_ranks", 4, collectives);
}
fn floats(g: Group) {
let key = Array::from_slice(&[0u32, g.rank() as u32 + 7], [2]);
let x = nuvec_core::random::normal([4099], DType::F32, 0.0, 1.0, &key);
for _ in 0..2 {
let s = distributed::all_sum(&x, None);
let bits: u64 = s
.to_vec::<f32>()
.unwrap()
.iter()
.fold(0u64, |h, v| h.rotate_left(5) ^ u64::from(v.to_bits()));
println!("BITS {bits:016x}");
}
}
#[test]
fn float_sums_agree_bit_for_bit() {
for n in [2, 3, 4] {
if let Some(outputs) = ranks("float_sums_agree_bit_for_bit", n, floats) {
agree(&outputs);
} else {
return;
}
}
}
fn large(g: Group) {
let n = 16 << 20;
let x = nuvec_core::ops::full([n], g.rank() as f32 + 0.5);
let s = distributed::all_sum(&x, None);
let want = (0..g.size()).map(|r| r as f32 + 0.5).sum::<f32>();
let v = s.to_vec::<f32>().unwrap();
assert!(v.iter().all(|&x| x == want), "{}", v[0]);
}
#[test]
fn large_arrays_on_two_and_four_ranks() {
for n in [2, 4] {
if ranks("large_arrays_on_two_and_four_ranks", n, large).is_none() {
return;
}
}
}
fn point_to_point(g: Group) {
let (rank, size) = (g.rank(), g.size());
let (right, left) = ((rank + 1) % size, (rank + size - 1) % size);
let x = Array::from_slice(&values(rank, 6), [2, 3]);
let sent = || distributed::send(&x, right, None).eval().unwrap();
let got = if rank % 2 == 0 {
sent();
distributed::recv([2, 3], DType::F32, left, None)
} else {
let r = distributed::recv_like(&x, left, None);
r.eval().unwrap();
sent();
r
};
assert_eq!(got.to_vec::<f32>().unwrap(), values(left, 6));
if rank == 0 {
distributed::send(&x, 1, None).eval().unwrap();
} else if rank == 1 {
let wrong = distributed::recv([3, 2], DType::F32, 0, None);
let err = wrong.eval().unwrap_err().to_string();
assert!(err.contains("shape"), "{err}");
}
if size == 4 {
let err = distributed::send(&x, (rank + 2) % 4, None)
.eval()
.unwrap_err()
.to_string();
assert!(err.contains("neighbours"), "{err}");
}
assert!(distributed::send(&x, rank, None).eval().is_err());
}
#[test]
fn send_and_recv_between_neighbours() {
for n in [2, 4] {
if ranks("send_and_recv_between_neighbours", n, point_to_point).is_none() {
return;
}
}
}
fn transforms(g: Group) {
let (rank, size) = (g.rank(), g.size());
let x = Array::from_slice(&values(rank, 4), [4]);
let total: Vec<f32> = (0..4)
.map(|i| (0..size).map(|r| values(r, 4)[i]).sum())
.collect();
let loss = |x: &Array| {
let s = distributed::all_sum(x, None);
s.multiply(&s).sum(..)
};
let dx = grad(loss)(&x).unwrap();
let want: Vec<f32> = total.iter().map(|t| 2.0 * t).collect();
assert_eq!(dx.to_vec::<f32>().unwrap(), want);
let h = 1e-2f32;
let mut e = vec![0.0f32; 4];
if rank == 0 {
e[1] = h;
}
let plus = loss(&x.add(Array::from_slice(&e, [4])))
.item::<f32>()
.unwrap();
let minus = loss(&x.subtract(Array::from_slice(&e, [4])))
.item::<f32>()
.unwrap();
if rank == 0 {
assert!(((plus - minus) / (2.0 * h) - want[1]).abs() < 1e-2);
}
let w = nuvec_core::ops::arange(0, 4 * size as i32, 1).astype(DType::F32);
let gathered_loss = |x: &Array| distributed::all_gather(x, None).multiply(&w).sum(..);
let dg = grad(gathered_loss)(&x).unwrap().to_vec::<f32>().unwrap();
let rows: Vec<f32> = (4 * rank..4 * rank + 4).map(|i| i as f32).collect();
assert_eq!(dg, rows);
let f = nuvec_compile::compile(|x: &Array| distributed::all_sum(x, None).multiply(2.0));
for k in 0..3 {
let y = x.add(k as f32);
let want = distributed::all_sum(&y, None).multiply(2.0);
assert_eq!(
f(&y).unwrap().to_vec::<f32>().unwrap(),
want.to_vec::<f32>().unwrap()
);
}
let batch = Array::from_slice(&values(rank, 8), [2, 4]);
let batched = vmap(|r: &Array| distributed::all_sum(r, None))
.call(&batch)
.unwrap();
assert_eq!(
batched.to_vec::<f32>().unwrap(),
distributed::all_sum(&batch, None).to_vec::<f32>().unwrap()
);
if cfg!(target_os = "macos") {
set_default_stream(Stream::default_for(Device::gpu(0)));
if (&Array::from(1.0f32) + 1.0).eval().is_ok() {
let y = x.multiply(3.0);
let s = distributed::all_sum(&y, None).add(1.0);
let want: Vec<f32> = total.iter().map(|t| 3.0 * t + 1.0).collect();
assert_eq!(s.to_vec::<f32>().unwrap(), want);
}
set_default_stream(Stream::default_for(Device::cpu()));
}
}
#[test]
fn collectives_compose_with_transforms() {
for n in [2, 3] {
if ranks("collectives_compose_with_transforms", n, transforms).is_none() {
return;
}
}
}
fn peer_exits(g: Group) {
if g.rank() == g.size() - 1 {
std::process::exit(0);
}
let x = array![1.0, 2.0];
let start = Instant::now();
let err = distributed::all_sum(&x, None)
.eval()
.unwrap_err()
.to_string();
assert!(err.contains("rank"), "{err}");
assert!(start.elapsed() < Duration::from_secs(25));
let start = Instant::now();
let err = distributed::all_sum(&x, None)
.eval()
.unwrap_err()
.to_string();
assert!(err.contains("failed earlier"), "{err}");
assert!(start.elapsed() < Duration::from_secs(1));
}
#[test]
fn a_peer_that_exits_fails_the_others() {
ranks("a_peer_that_exits_fails_the_others", 3, peer_exits);
}
#[test]
fn a_single_process_runs_unchanged() {
if std::env::var(CHILD).is_ok() {
return;
}
let g = nuvec_distributed::init(false).unwrap();
assert_eq!((g.rank(), g.size()), (0, 1));
assert!(nuvec_distributed::init(true).is_err());
let x = array![[1.0, 2.0], [3.0, 4.0]];
for y in [
distributed::all_sum(&x, None),
distributed::all_max(&x, None),
distributed::all_gather(&x, None),
] {
assert_eq!(y.to_vec::<f32>().unwrap(), [1.0, 2.0, 3.0, 4.0]);
}
assert!(distributed::send(&x, 1, None).eval().is_err());
}
#[test]
fn a_rank_that_never_starts_fails_init() {
if std::env::var(CHILD).is_ok() {
return;
}
let ports: Vec<u16> = (0..2)
.map(|_| {
TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
})
.collect();
let nodes: Vec<Vec<String>> = ports
.iter()
.map(|p| vec![format!("127.0.0.1:{p}")])
.collect();
let options = nuvec_distributed::Options::default().timeout(Duration::from_secs(1));
let start = Instant::now();
let err = nuvec_distributed::init_with(&nodes, 0, &options)
.unwrap_err()
.to_string();
assert!(err.contains("rank 1"), "{err}");
assert!(start.elapsed() < Duration::from_secs(10));
}