use std::panic::Location;
use std::sync::{Arc, Mutex, PoisonError, RwLock};
use crate::array::Data;
use crate::buffer::Buffer;
use crate::kernels::Operand;
use crate::primitive::Primitive;
use crate::{Array, DType, Error, IntoShape, Result};
mod worker;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum AllReduce {
Sum,
Max,
Min,
}
pub trait Communicator: Send + Sync {
fn rank(&self) -> usize;
fn size(&self) -> usize;
fn all_reduce(&self, op: AllReduce, dtype: DType, data: &mut [u8]) -> Result<()>;
fn all_gather(&self, input: &[u8], output: &mut [u8]) -> Result<()>;
fn send(&self, dst: usize, data: &[u8]) -> Result<()>;
fn recv(&self, src: usize, data: &mut [u8]) -> Result<()>;
fn sum_scatter(&self, dtype: DType, input: &[u8], output: &mut [u8]) -> Result<()> {
let _ = (dtype, input, output);
Err(Error::InvalidArgument(
"distributed: this backend has no sum_scatter".into(),
))
}
fn health(&self) -> Result<()> {
Ok(())
}
fn split(&self, color: i32, key: i32) -> Result<Arc<dyn Communicator>> {
let _ = (color, key);
Err(Error::InvalidArgument(
"distributed: this backend does not split groups (as MLX's ring)".into(),
))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CollectiveOp {
Reduce(AllReduce),
Gather,
SumScatter,
Send {
dst: usize,
},
Recv {
src: usize,
shape: Vec<usize>,
dtype: DType,
},
}
impl CollectiveOp {
pub fn name(&self) -> &'static str {
match self {
Self::Reduce(AllReduce::Sum) => "all_sum",
Self::Reduce(AllReduce::Max) => "all_max",
Self::Reduce(AllReduce::Min) => "all_min",
Self::Gather => "all_gather",
Self::SumScatter => "sum_scatter",
Self::Send { .. } => "send",
Self::Recv { .. } => "recv",
}
}
}
struct Registered {
comm: Arc<dyn Communicator>,
order: Mutex<()>,
worker: Arc<worker::Worker>,
}
impl Drop for Registered {
fn drop(&mut self) {
self.worker.shut_down();
}
}
static GROUPS: Mutex<Vec<Option<Arc<Registered>>>> = Mutex::new(Vec::new());
static DEFAULT: RwLock<Option<Group>> = RwLock::new(None);
fn registered(id: usize) -> Result<Arc<Registered>> {
let groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
match id.checked_sub(1).and_then(|i| groups.get(i)) {
Some(Some(g)) => Ok(g.clone()),
Some(None) => Err(Error::Backend("distributed: the group was closed".into())),
None => Err(Error::InvalidArgument(format!(
"distributed: no group {id}"
))),
}
}
#[derive(Clone)]
pub struct Group {
id: usize,
rank: usize,
size: usize,
}
impl std::fmt::Debug for Group {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Group(rank {} of {})", self.rank, self.size)
}
}
impl Group {
pub fn single() -> Self {
Self {
id: 0,
rank: 0,
size: 1,
}
}
pub fn new(comm: Arc<dyn Communicator>) -> Self {
let (rank, size) = (comm.rank(), comm.size());
if size <= 1 {
return Self::single();
}
let mut groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
groups.push(Some(Arc::new(Registered {
comm,
order: Mutex::new(()),
worker: Arc::default(),
})));
Self {
id: groups.len(),
rank,
size,
}
}
pub fn rank(&self) -> usize {
self.rank
}
pub fn size(&self) -> usize {
self.size
}
pub fn close(&self) {
if let Some(i) = self.id.checked_sub(1) {
let mut groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(slot) = groups.get_mut(i) {
*slot = None;
}
}
}
pub fn health(&self) -> Result<()> {
if self.id == 0 {
return Ok(());
}
let entry = registered(self.id)?;
let _order = entry.worker.idle_order(&entry.order);
entry.comm.health()
}
pub fn split(&self, color: i32, key: i32) -> Result<Group> {
if self.size == 1 {
return Ok(self.clone());
}
Ok(Group::new(registered(self.id)?.comm.split(color, key)?))
}
}
pub fn set_default_group(group: Group) {
*DEFAULT.write().unwrap_or_else(PoisonError::into_inner) = Some(group);
}
pub fn default_group() -> Group {
DEFAULT
.read()
.unwrap_or_else(PoisonError::into_inner)
.clone()
.unwrap_or_else(Group::single)
}
fn group_or_default(group: Option<&Group>) -> Group {
group.cloned().unwrap_or_else(default_group)
}
pub(crate) fn group_size(id: usize) -> Result<usize> {
match id {
0 => Ok(1),
id => Ok(registered(id)?.comm.size()),
}
}
pub(crate) fn group_rank(id: usize) -> Result<usize> {
match id {
0 => Ok(0),
id => Ok(registered(id)?.comm.rank()),
}
}
fn record(op: CollectiveOp, group: &Group, inputs: Vec<Array>) -> Result<Array> {
let mut out = Array::from_op(
Primitive::Collective {
op,
group: group.id,
},
inputs,
)?;
Ok(out.swap_remove(0))
}
fn reduce(x: &Array, op: AllReduce, group: Option<&Group>) -> Result<Array> {
let group = group_or_default(group);
if group.size == 1 {
return Ok(x.clone());
}
record(CollectiveOp::Reduce(op), &group, vec![x.clone()])
}
fn gather(x: &Array, group: Option<&Group>) -> Result<Array> {
let group = group_or_default(group);
if group.size == 1 {
return Ok(x.clone());
}
record(CollectiveOp::Gather, &group, vec![x.clone()])
}
fn scatter(x: &Array, group: Option<&Group>) -> Result<Array> {
let group = group_or_default(group);
if group.size == 1 {
return Ok(x.clone());
}
if x.dims().first().is_none_or(|d| d % group.size != 0) {
return Err(Error::InvalidArgument(format!(
"sum_scatter: shape {:?} is invalid for a group of size {}: the first axis must \
exist and be divisible by the group size",
x.dims(),
group.size
)));
}
record(CollectiveOp::SumScatter, &group, vec![x.clone()])
}
fn peer(op: &str, group: &Group, peer: usize) -> Result<()> {
if group.size == 1 || peer >= group.size || peer == group.rank {
return Err(Error::InvalidArgument(format!(
"{op}: rank {peer} is not another rank of a group of {}",
group.size
)));
}
Ok(())
}
fn send_op(x: &Array, dst: usize, group: Option<&Group>) -> Result<Array> {
let group = group_or_default(group);
peer("send", &group, dst)?;
record(CollectiveOp::Send { dst }, &group, vec![x.clone()])
}
fn recv_op(shape: Vec<usize>, dtype: DType, src: usize, group: Option<&Group>) -> Result<Array> {
let group = group_or_default(group);
peer("recv", &group, src)?;
record(CollectiveOp::Recv { src, shape, dtype }, &group, Vec::new())
}
const HEADER: usize = 512;
fn header(dtype: DType, shape: &[usize]) -> Result<Vec<u8>> {
let name = dtype.name().as_bytes();
let n: usize = shape.iter().product();
let mut h = Vec::with_capacity(HEADER);
h.extend_from_slice(b"nuvecsnd");
h.extend_from_slice(&((n * dtype.size()) as u64).to_le_bytes());
h.extend_from_slice(&(name.len() as u64).to_le_bytes());
h.extend_from_slice(name);
h.extend_from_slice(&(shape.len() as u64).to_le_bytes());
for &d in shape {
h.extend_from_slice(&(d as u64).to_le_bytes());
}
if h.len() > HEADER {
return Err(Error::InvalidArgument(format!(
"send: {} dimensions do not fit the array header",
shape.len()
)));
}
h.resize(HEADER, 0);
Ok(h)
}
fn output_len(op: &CollectiveOp, size: usize, operands: &[Operand<'_>]) -> usize {
match op {
CollectiveOp::Reduce(_) | CollectiveOp::Send { .. } => {
operands[0].0.size * operands[0].0.dtype.size()
}
CollectiveOp::Gather => operands[0].0.size * operands[0].0.dtype.size() * size,
CollectiveOp::SumScatter => operands[0].0.size * operands[0].0.dtype.size() / size,
CollectiveOp::Recv { shape, dtype, .. } => shape.iter().product::<usize>() * dtype.size(),
}
}
fn eval_bytes(
op: &CollectiveOp,
comm: &dyn Communicator,
dtype: DType,
shape: &[usize],
input: Option<&[u8]>,
out: &mut [u8],
) -> Result<()> {
let input_or_out = |out: &mut [u8]| {
if let Some(input) = input {
out.copy_from_slice(input);
}
};
match op {
CollectiveOp::Reduce(reduce) => {
input_or_out(out);
comm.all_reduce(*reduce, dtype, out)
}
CollectiveOp::Gather => comm.all_gather(input.expect("an input"), out),
CollectiveOp::SumScatter => comm.sum_scatter(dtype, input.expect("an input"), out),
CollectiveOp::Send { dst } => {
input_or_out(out);
comm.send(*dst, &header(dtype, shape)?)?;
comm.send(*dst, out)
}
CollectiveOp::Recv { src, shape, dtype } => {
let want = header(*dtype, shape)?;
let mut got = vec![0u8; HEADER];
comm.recv(*src, &mut got)?;
if &got[..8] != b"nuvecsnd" {
return Err(Error::InvalidArgument(format!(
"recv: rank {src} sent no array header"
)));
}
if got != want {
let bytes = u64::from_le_bytes(got[8..16].try_into().expect("8 bytes")) as usize;
let mut sink = vec![0u8; bytes.min(1 << 20)];
let mut left = bytes;
while left > 0 {
let n = left.min(sink.len());
comm.recv(*src, &mut sink[..n])?;
left -= n;
}
return Err(Error::InvalidArgument(format!(
"recv: rank {src} sent another dtype or shape than {} {shape:?}",
dtype.name()
)));
}
comm.recv(*src, out)
}
}
}
fn eval_into(
op: &CollectiveOp,
comm: &dyn Communicator,
operands: &[Operand<'_>],
out: &mut [u8],
) -> Result<()> {
let Some(&(meta, data)) = operands.first() else {
return eval_bytes(op, comm, DType::U8, &[], None, out);
};
let elem = meta.dtype.size();
let bytes = usize::try_from(data.offset).ok().and_then(|at| {
let at = at * elem;
let bytes = data.buffer.bytes().get(at..at + meta.size * elem);
bytes.filter(|_| meta.flags.row_contiguous)
});
if let Some(bytes) = bytes {
return eval_bytes(op, comm, meta.dtype, &meta.shape, Some(bytes), out);
}
let input = crate::kernels::contiguous(meta, data)?;
eval_bytes(op, comm, meta.dtype, &meta.shape, Some(input.bytes()), out)
}
pub(crate) fn eval_cpu(
op: &CollectiveOp,
group: usize,
operands: &[Operand<'_>],
) -> Result<Vec<Data>> {
let entry = registered(group)?;
let mut out = Buffer::try_alloc(output_len(op, entry.comm.size(), operands))?;
let _order = entry.worker.idle_order(&entry.order);
eval_into(op, &*entry.comm, operands, out.bytes_mut())?;
Ok(vec![Data::new(out)])
}
#[cfg(all(target_os = "macos", not(miri)))]
pub(crate) enum Ready {
Now,
Signal(Arc<crate::gpu::BufferSignal>, u64),
Pending(Option<nuvec_gpu::Pending>),
}
#[cfg(all(target_os = "macos", not(miri)))]
pub(crate) enum Input {
InOutput,
Staged(Data),
Operand(Array, Data),
}
#[cfg(all(target_os = "macos", not(miri)))]
pub(crate) struct GpuJob {
pub(crate) op: CollectiveOp,
pub(crate) dtype: DType,
pub(crate) shape: Vec<usize>,
pub(crate) ready: Ready,
pub(crate) input: Input,
pub(crate) out: Data,
pub(crate) status: crate::array::ChunkStatus,
}
#[cfg(all(target_os = "macos", not(miri)))]
pub(crate) fn submit_gpu(
group: usize,
job: GpuJob,
device: &nuvec_gpu::Device,
) -> Result<(Arc<dyn std::any::Any + Send + Sync>, u64)> {
let entry = registered(group)?;
entry.worker.event(device)?;
let worker = entry.worker.clone();
let value = worker.submit(move |value| {
let GpuJob {
op,
dtype,
shape,
ready,
input,
out,
status,
} = job;
let run = || {
let ready = match ready {
Ready::Now => Ok(()),
Ready::Signal(signal, v) => signal.wait(v),
Ready::Pending(p) => p.map_or(Ok(()), |p| {
p.wait_result().map_err(|e| Error::Backend(e.to_string()))
}),
};
let _order = entry.order.lock().unwrap_or_else(PoisonError::into_inner);
let ran = write_unsynchronized(&out, |bytes| match &input {
Input::InOutput => eval_bytes(&op, &*entry.comm, dtype, &shape, None, bytes),
Input::Staged(staged) => read_unsynchronized(staged, |i| {
eval_bytes(&op, &*entry.comm, dtype, &shape, Some(i), bytes)
}),
Input::Operand(a, d) => eval_into(&op, &*entry.comm, &[(a.out(), d)], bytes),
});
ready.and(ran)
};
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run))
.unwrap_or_else(|_| Err(Error::Backend("distributed: a collective panicked".into())));
if let Err(e) = result {
let _ = status.set(e);
}
if let Some(event) = entry.worker.event.get() {
event.signal(value);
}
drop((input, out));
});
Ok((worker, value))
}
#[cfg(all(target_os = "macos", not(miri)))]
pub(crate) fn worker_event(worker: &(dyn std::any::Any + Send + Sync)) -> &nuvec_gpu::Event {
worker
.downcast_ref::<worker::Worker>()
.and_then(|w| w.event.get())
.expect("created by submit_gpu")
}
#[cfg(all(target_os = "macos", not(miri)))]
#[allow(unsafe_code)]
fn read_unsynchronized(staged: &Data, f: impl FnOnce(&[u8]) -> Result<()>) -> Result<()> {
let len = staged.buffer.len();
let Some(metal) = staged.buffer.metal() else {
return f(staged.buffer.bytes());
};
let (ptr, available) = metal
.unsynchronized_ptr()
.map_err(|e| Error::Backend(e.to_string()))?;
assert!(len <= available, "the staging buffer holds its bytes");
f(unsafe { std::slice::from_raw_parts(ptr.as_ptr(), len) })
}
#[cfg(all(target_os = "macos", not(miri)))]
#[allow(unsafe_code)]
fn write_unsynchronized(out: &Data, f: impl FnOnce(&mut [u8]) -> Result<()>) -> Result<()> {
let len = out.buffer.len();
let Some(metal) = out.buffer.metal() else {
return Err(Error::Backend(
"distributed: a GPU collective's output is not on the GPU".into(),
));
};
let (ptr, available) = metal
.unsynchronized_ptr()
.map_err(|e| Error::Backend(e.to_string()))?;
assert!(len <= available, "the output buffer holds its bytes");
let bytes = unsafe { std::slice::from_raw_parts_mut(ptr.as_ptr(), len) };
f(bytes)
}
#[track_caller]
pub fn all_sum(x: &Array, group: Option<&Group>) -> Array {
Array::sticky("all_sum", Location::caller(), &[x], || {
reduce(x, AllReduce::Sum, group)
})
}
#[track_caller]
pub fn all_max(x: &Array, group: Option<&Group>) -> Array {
Array::sticky("all_max", Location::caller(), &[x], || {
reduce(x, AllReduce::Max, group)
})
}
#[track_caller]
pub fn all_min(x: &Array, group: Option<&Group>) -> Array {
Array::sticky("all_min", Location::caller(), &[x], || {
reduce(x, AllReduce::Min, group)
})
}
#[track_caller]
pub fn all_gather(x: &Array, group: Option<&Group>) -> Array {
Array::sticky("all_gather", Location::caller(), &[x], || gather(x, group))
}
#[track_caller]
pub fn sum_scatter(x: &Array, group: Option<&Group>) -> Array {
Array::sticky("sum_scatter", Location::caller(), &[x], || {
scatter(x, group)
})
}
#[track_caller]
pub fn send(x: &Array, dst: usize, group: Option<&Group>) -> Array {
Array::sticky("send", Location::caller(), &[x], || send_op(x, dst, group))
}
#[track_caller]
pub fn recv(shape: impl IntoShape, dtype: DType, src: usize, group: Option<&Group>) -> Array {
let spec = shape.shape_spec();
Array::constructor("recv", Location::caller(), dtype, &spec, |shape| {
recv_op(shape.to_vec(), dtype, src, group)
})
}
#[track_caller]
pub fn recv_like(x: &Array, src: usize, group: Option<&Group>) -> Array {
let at = Location::caller();
Array::sticky("recv_like", at, &[x], || {
recv_op(x.dims().to_vec(), x.dtype(), src, group)
})
}
#[track_caller]
pub fn sum_gradients(x: &Array, group: Option<&Group>) -> Array {
let group = group_or_default(group);
if group.size == 1 {
return x.clone();
}
Array::sticky("sum_gradients", Location::caller(), &[x], || {
let f = crate::custom_function_flat(|xs| Ok(xs.to_vec())).with_vjp(
move |_primals, _outputs, cotangents| {
cotangents
.iter()
.map(|c| reduce(c, AllReduce::Sum, Some(&group)))
.collect()
},
);
Ok(f.call(std::slice::from_ref(x))?.remove(0))
})
}
impl crate::tree::Static for Group {
fn hash_static(&self, mut h: &mut dyn std::hash::Hasher) {
std::hash::Hash::hash(&(self.id, self.rank, self.size), &mut h);
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use std::time::Duration;
use super::*;
struct Fake {
log: Mutex<Vec<String>>,
fail_at: Option<usize>,
}
impl Communicator for Fake {
fn rank(&self) -> usize {
0
}
fn size(&self) -> usize {
2
}
fn all_reduce(&self, op: AllReduce, dtype: DType, data: &mut [u8]) -> Result<()> {
std::thread::sleep(Duration::from_millis(2));
let n = data.len() / dtype.size();
self.log.lock().unwrap().push(format!("reduce {n}"));
if self.fail_at == Some(n) {
return Err(Error::Backend("distributed: rank 1 left".into()));
}
if op == AllReduce::Sum && dtype == DType::F32 {
for v in bytemuck::cast_slice_mut::<u8, f32>(data) {
*v *= 2.0;
}
}
Ok(())
}
fn all_gather(&self, input: &[u8], output: &mut [u8]) -> Result<()> {
output[..input.len()].copy_from_slice(input);
output[input.len()..].copy_from_slice(input);
Ok(())
}
fn send(&self, _: usize, _: &[u8]) -> Result<()> {
Ok(())
}
fn recv(&self, _: usize, data: &mut [u8]) -> Result<()> {
data.fill(0);
Ok(())
}
}
fn fake(fail_at: Option<usize>) -> (Group, Arc<Fake>) {
let comm = Arc::new(Fake {
log: Mutex::new(Vec::new()),
fail_at,
});
(Group::new(comm.clone()), comm)
}
fn gpu() -> bool {
if !crate::gpu::available() {
return false;
}
crate::set_default_stream(crate::Stream::default_for(crate::Device::gpu(0)));
true
}
#[test]
fn collectives_run_in_program_order_across_streams() {
if !gpu() {
return;
}
let (g, comm) = fake(None);
let x = |n: usize| crate::ops::ones([n as i32], DType::F32);
let a = all_sum(&x(1).multiply(3.0), Some(&g));
let b = all_sum(&x(2).multiply(3.0), Some(&g));
crate::async_eval(&a).unwrap();
crate::async_eval(&b).unwrap();
crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
let c = all_sum(&x(3), Some(&g));
assert_eq!(c.to_vec::<f32>().unwrap(), [2.0; 3]);
assert_eq!(a.to_vec::<f32>().unwrap(), [6.0]);
assert_eq!(b.to_vec::<f32>().unwrap(), [6.0; 2]);
assert_eq!(
*comm.log.lock().unwrap(),
["reduce 1", "reduce 2", "reduce 3"]
);
}
#[test]
fn a_dropped_result_still_runs() {
if !gpu() {
return;
}
let (g, comm) = fake(None);
let a = all_sum(&crate::ops::ones([4], DType::F32).multiply(2.0), Some(&g));
crate::async_eval(&a).unwrap();
drop(a);
let b = all_sum(&crate::ops::ones([5], DType::F32).add(1.0), Some(&g));
assert_eq!(b.to_vec::<f32>().unwrap(), [4.0; 5]);
assert_eq!(*comm.log.lock().unwrap(), ["reduce 4", "reduce 5"]);
}
#[test]
fn a_failed_collective_fails_what_follows() {
if !gpu() {
return;
}
let (g, _) = fake(Some(7));
let y = all_sum(&crate::ops::ones([7], DType::F32).multiply(2.0), Some(&g)).add(1.0);
let err = y.eval().unwrap_err().to_string();
assert!(err.contains("rank 1 left"), "{err}");
}
#[cfg(all(target_os = "macos", not(miri)))]
#[test]
fn a_gpu_failure_before_a_collective_fails_it() {
if !gpu() {
return;
}
let (g, comm) = fake(None);
let x = crate::ops::ones([6], DType::F32).multiply(2.0);
crate::gpu::fault::fail_next_commit();
let y = all_sum(&x, Some(&g)).add(1.0);
assert!(y.eval().is_err());
assert_eq!(*comm.log.lock().unwrap(), ["reduce 6"]);
}
#[cfg(all(target_os = "macos", not(miri)))]
#[test]
fn collectives_do_not_commit_command_buffers() {
if !gpu() {
return;
}
let (g, _) = fake(None);
let x = crate::ops::ones([1, 1024], DType::F32);
x.eval().unwrap();
let chain = |x: &Array, collectives: bool| {
let mut y = x.clone();
for _ in 0..56 {
y = y.multiply(0.5);
if collectives {
y = all_sum(&y, Some(&g));
}
}
y
};
chain(&x, true).eval().unwrap();
chain(&x, false).eval().unwrap();
let count = |collectives: bool| {
let before = crate::gpu::COMMITS.get();
let y = chain(&x, collectives);
y.eval().unwrap();
(crate::gpu::COMMITS.get() - before, y)
};
let (with, y) = count(true);
let (without, _) = count(false);
assert!(
with <= without + 56 / 8,
"{with} command buffers for 56 collectives, {without} without them"
);
assert_eq!(y.to_vec::<f32>().unwrap(), vec![1.0; 1024]);
}
#[test]
fn a_cpu_collective_after_a_gpu_one_in_one_evaluation() {
if !gpu() {
return;
}
let (g, comm) = fake(None);
let a = all_sum(&crate::ops::ones([3], DType::F32).multiply(3.0), Some(&g));
crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
let c = all_sum(&a.add(1.0), Some(&g));
assert_eq!(c.to_vec::<f32>().unwrap(), [14.0; 3]);
assert_eq!(*comm.log.lock().unwrap(), ["reduce 3", "reduce 3"]);
}
#[test]
fn only_inputs_nothing_else_holds_are_reduced_in_place() {
if !gpu() {
return;
}
let (g, _comm) = fake(None);
let held = crate::ops::ones([5], DType::F32).multiply(3.0);
held.eval().unwrap();
let sum = all_sum(&held, Some(&g));
assert_eq!(sum.to_vec::<f32>().unwrap(), [6.0; 5]);
assert_eq!(held.to_vec::<f32>().unwrap(), [3.0; 5]);
let intermediate = all_sum(&crate::ops::ones([5], DType::F32).multiply(2.0), Some(&g));
assert_eq!(intermediate.to_vec::<f32>().unwrap(), [4.0; 5]);
let done = crate::ops::ones([5], DType::F32).multiply(5.0);
done.eval().unwrap();
let sum = all_sum(&done, Some(&g));
drop(done);
assert_eq!(sum.to_vec::<f32>().unwrap(), [10.0; 5]);
let x = crate::ops::ones([5], DType::F32).multiply(4.0);
let (sum, other) = (all_sum(&x, Some(&g)), x.add(1.0));
drop(x);
crate::eval(&[sum.clone(), other.clone()]).unwrap();
assert_eq!(sum.to_vec::<f32>().unwrap(), [8.0; 5]);
assert_eq!(other.to_vec::<f32>().unwrap(), [5.0; 5]);
}
#[cfg(all(target_os = "macos", not(miri)))]
#[test]
fn a_failed_buffer_fails_every_collective_in_it() {
if !gpu() {
return;
}
let (g, comm) = fake(None);
let mut y = crate::ops::ones([8], DType::F32);
for _ in 0..3 {
y = all_sum(&y.multiply(2.0), Some(&g));
}
crate::gpu::fault::fail_next_commit();
assert!(y.eval().is_err());
assert_eq!(comm.log.lock().unwrap().len(), 3);
}
}