1use std::panic::Location;
16use std::sync::{Arc, Mutex, PoisonError, RwLock};
17
18use crate::array::Data;
19use crate::buffer::Buffer;
20use crate::kernels::Operand;
21use crate::primitive::Primitive;
22use crate::{Array, DType, Error, IntoShape, Result};
23
24mod worker;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub enum AllReduce {
30 Sum,
32 Max,
34 Min,
36}
37
38pub trait Communicator: Send + Sync {
41 fn rank(&self) -> usize;
43 fn size(&self) -> usize;
45 fn all_reduce(&self, op: AllReduce, dtype: DType, data: &mut [u8]) -> Result<()>;
48 fn all_gather(&self, input: &[u8], output: &mut [u8]) -> Result<()>;
50 fn send(&self, dst: usize, data: &[u8]) -> Result<()>;
52 fn recv(&self, src: usize, data: &mut [u8]) -> Result<()>;
54 fn sum_scatter(&self, dtype: DType, input: &[u8], output: &mut [u8]) -> Result<()> {
58 let _ = (dtype, input, output);
59 Err(Error::InvalidArgument(
60 "distributed: this backend has no sum_scatter".into(),
61 ))
62 }
63 fn health(&self) -> Result<()> {
66 Ok(())
67 }
68 fn split(&self, color: i32, key: i32) -> Result<Arc<dyn Communicator>> {
70 let _ = (color, key);
71 Err(Error::InvalidArgument(
72 "distributed: this backend does not split groups (as MLX's ring)".into(),
73 ))
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79#[non_exhaustive]
80pub enum CollectiveOp {
81 Reduce(AllReduce),
83 Gather,
85 SumScatter,
87 Send {
89 dst: usize,
91 },
92 Recv {
94 src: usize,
96 shape: Vec<usize>,
98 dtype: DType,
100 },
101}
102
103impl CollectiveOp {
104 pub fn name(&self) -> &'static str {
106 match self {
107 Self::Reduce(AllReduce::Sum) => "all_sum",
108 Self::Reduce(AllReduce::Max) => "all_max",
109 Self::Reduce(AllReduce::Min) => "all_min",
110 Self::Gather => "all_gather",
111 Self::SumScatter => "sum_scatter",
112 Self::Send { .. } => "send",
113 Self::Recv { .. } => "recv",
114 }
115 }
116}
117
118struct Registered {
121 comm: Arc<dyn Communicator>,
122 order: Mutex<()>,
123 worker: Arc<worker::Worker>,
125}
126
127impl Drop for Registered {
128 fn drop(&mut self) {
129 self.worker.shut_down();
130 }
131}
132
133static GROUPS: Mutex<Vec<Option<Arc<Registered>>>> = Mutex::new(Vec::new());
135static DEFAULT: RwLock<Option<Group>> = RwLock::new(None);
136
137fn registered(id: usize) -> Result<Arc<Registered>> {
138 let groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
139 match id.checked_sub(1).and_then(|i| groups.get(i)) {
140 Some(Some(g)) => Ok(g.clone()),
141 Some(None) => Err(Error::Backend("distributed: the group was closed".into())),
142 None => Err(Error::InvalidArgument(format!(
143 "distributed: no group {id}"
144 ))),
145 }
146}
147
148#[derive(Clone)]
150pub struct Group {
151 id: usize,
152 rank: usize,
153 size: usize,
154}
155
156impl std::fmt::Debug for Group {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 write!(f, "Group(rank {} of {})", self.rank, self.size)
159 }
160}
161
162impl Group {
163 pub fn single() -> Self {
165 Self {
166 id: 0,
167 rank: 0,
168 size: 1,
169 }
170 }
171
172 pub fn new(comm: Arc<dyn Communicator>) -> Self {
174 let (rank, size) = (comm.rank(), comm.size());
175 if size <= 1 {
176 return Self::single();
177 }
178 let mut groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
179 groups.push(Some(Arc::new(Registered {
180 comm,
181 order: Mutex::new(()),
182 worker: Arc::default(),
183 })));
184 Self {
185 id: groups.len(),
186 rank,
187 size,
188 }
189 }
190
191 pub fn rank(&self) -> usize {
193 self.rank
194 }
195
196 pub fn size(&self) -> usize {
198 self.size
199 }
200
201 pub fn close(&self) {
205 if let Some(i) = self.id.checked_sub(1) {
206 let mut groups = GROUPS.lock().unwrap_or_else(PoisonError::into_inner);
207 if let Some(slot) = groups.get_mut(i) {
208 *slot = None;
209 }
210 }
211 }
212
213 pub fn health(&self) -> Result<()> {
217 if self.id == 0 {
218 return Ok(());
219 }
220 let entry = registered(self.id)?;
221 let _order = entry.worker.idle_order(&entry.order);
222 entry.comm.health()
223 }
224
225 pub fn split(&self, color: i32, key: i32) -> Result<Group> {
228 if self.size == 1 {
229 return Ok(self.clone());
230 }
231 Ok(Group::new(registered(self.id)?.comm.split(color, key)?))
232 }
233}
234
235pub fn set_default_group(group: Group) {
238 *DEFAULT.write().unwrap_or_else(PoisonError::into_inner) = Some(group);
239}
240
241pub fn default_group() -> Group {
243 DEFAULT
244 .read()
245 .unwrap_or_else(PoisonError::into_inner)
246 .clone()
247 .unwrap_or_else(Group::single)
248}
249
250fn group_or_default(group: Option<&Group>) -> Group {
251 group.cloned().unwrap_or_else(default_group)
252}
253
254pub(crate) fn group_size(id: usize) -> Result<usize> {
256 match id {
257 0 => Ok(1),
258 id => Ok(registered(id)?.comm.size()),
259 }
260}
261
262pub(crate) fn group_rank(id: usize) -> Result<usize> {
264 match id {
265 0 => Ok(0),
266 id => Ok(registered(id)?.comm.rank()),
267 }
268}
269
270fn record(op: CollectiveOp, group: &Group, inputs: Vec<Array>) -> Result<Array> {
271 let mut out = Array::from_op(
272 Primitive::Collective {
273 op,
274 group: group.id,
275 },
276 inputs,
277 )?;
278 Ok(out.swap_remove(0))
279}
280
281fn reduce(x: &Array, op: AllReduce, group: Option<&Group>) -> Result<Array> {
282 let group = group_or_default(group);
283 if group.size == 1 {
284 return Ok(x.clone());
285 }
286 record(CollectiveOp::Reduce(op), &group, vec![x.clone()])
287}
288
289fn gather(x: &Array, group: Option<&Group>) -> Result<Array> {
290 let group = group_or_default(group);
291 if group.size == 1 {
292 return Ok(x.clone());
293 }
294 record(CollectiveOp::Gather, &group, vec![x.clone()])
295}
296
297fn scatter(x: &Array, group: Option<&Group>) -> Result<Array> {
298 let group = group_or_default(group);
299 if group.size == 1 {
300 return Ok(x.clone());
301 }
302 if x.dims().first().is_none_or(|d| d % group.size != 0) {
303 return Err(Error::InvalidArgument(format!(
304 "sum_scatter: shape {:?} is invalid for a group of size {}: the first axis must \
305 exist and be divisible by the group size",
306 x.dims(),
307 group.size
308 )));
309 }
310 record(CollectiveOp::SumScatter, &group, vec![x.clone()])
311}
312
313fn peer(op: &str, group: &Group, peer: usize) -> Result<()> {
314 if group.size == 1 || peer >= group.size || peer == group.rank {
315 return Err(Error::InvalidArgument(format!(
316 "{op}: rank {peer} is not another rank of a group of {}",
317 group.size
318 )));
319 }
320 Ok(())
321}
322
323fn send_op(x: &Array, dst: usize, group: Option<&Group>) -> Result<Array> {
324 let group = group_or_default(group);
325 peer("send", &group, dst)?;
326 record(CollectiveOp::Send { dst }, &group, vec![x.clone()])
327}
328
329fn recv_op(shape: Vec<usize>, dtype: DType, src: usize, group: Option<&Group>) -> Result<Array> {
330 let group = group_or_default(group);
331 peer("recv", &group, src)?;
332 record(CollectiveOp::Recv { src, shape, dtype }, &group, Vec::new())
333}
334
335const HEADER: usize = 512;
337
338fn header(dtype: DType, shape: &[usize]) -> Result<Vec<u8>> {
343 let name = dtype.name().as_bytes();
344 let n: usize = shape.iter().product();
345 let mut h = Vec::with_capacity(HEADER);
346 h.extend_from_slice(b"nuvecsnd");
347 h.extend_from_slice(&((n * dtype.size()) as u64).to_le_bytes());
348 h.extend_from_slice(&(name.len() as u64).to_le_bytes());
349 h.extend_from_slice(name);
350 h.extend_from_slice(&(shape.len() as u64).to_le_bytes());
351 for &d in shape {
352 h.extend_from_slice(&(d as u64).to_le_bytes());
353 }
354 if h.len() > HEADER {
355 return Err(Error::InvalidArgument(format!(
356 "send: {} dimensions do not fit the array header",
357 shape.len()
358 )));
359 }
360 h.resize(HEADER, 0);
361 Ok(h)
362}
363
364fn output_len(op: &CollectiveOp, size: usize, operands: &[Operand<'_>]) -> usize {
366 match op {
367 CollectiveOp::Reduce(_) | CollectiveOp::Send { .. } => {
368 operands[0].0.size * operands[0].0.dtype.size()
369 }
370 CollectiveOp::Gather => operands[0].0.size * operands[0].0.dtype.size() * size,
371 CollectiveOp::SumScatter => operands[0].0.size * operands[0].0.dtype.size() / size,
372 CollectiveOp::Recv { shape, dtype, .. } => shape.iter().product::<usize>() * dtype.size(),
373 }
374}
375
376fn eval_bytes(
380 op: &CollectiveOp,
381 comm: &dyn Communicator,
382 dtype: DType,
383 shape: &[usize],
384 input: Option<&[u8]>,
385 out: &mut [u8],
386) -> Result<()> {
387 let input_or_out = |out: &mut [u8]| {
388 if let Some(input) = input {
389 out.copy_from_slice(input);
390 }
391 };
392 match op {
393 CollectiveOp::Reduce(reduce) => {
394 input_or_out(out);
395 comm.all_reduce(*reduce, dtype, out)
396 }
397 CollectiveOp::Gather => comm.all_gather(input.expect("an input"), out),
398 CollectiveOp::SumScatter => comm.sum_scatter(dtype, input.expect("an input"), out),
399 CollectiveOp::Send { dst } => {
400 input_or_out(out);
401 comm.send(*dst, &header(dtype, shape)?)?;
402 comm.send(*dst, out)
403 }
404 CollectiveOp::Recv { src, shape, dtype } => {
405 let want = header(*dtype, shape)?;
406 let mut got = vec![0u8; HEADER];
407 comm.recv(*src, &mut got)?;
408 if &got[..8] != b"nuvecsnd" {
409 return Err(Error::InvalidArgument(format!(
410 "recv: rank {src} sent no array header"
411 )));
412 }
413 if got != want {
414 let bytes = u64::from_le_bytes(got[8..16].try_into().expect("8 bytes")) as usize;
416 let mut sink = vec![0u8; bytes.min(1 << 20)];
417 let mut left = bytes;
418 while left > 0 {
419 let n = left.min(sink.len());
420 comm.recv(*src, &mut sink[..n])?;
421 left -= n;
422 }
423 return Err(Error::InvalidArgument(format!(
424 "recv: rank {src} sent another dtype or shape than {} {shape:?}",
425 dtype.name()
426 )));
427 }
428 comm.recv(*src, out)
429 }
430 }
431}
432
433fn eval_into(
435 op: &CollectiveOp,
436 comm: &dyn Communicator,
437 operands: &[Operand<'_>],
438 out: &mut [u8],
439) -> Result<()> {
440 let Some(&(meta, data)) = operands.first() else {
441 return eval_bytes(op, comm, DType::U8, &[], None, out);
442 };
443 let elem = meta.dtype.size();
446 let bytes = usize::try_from(data.offset).ok().and_then(|at| {
447 let at = at * elem;
448 let bytes = data.buffer.bytes().get(at..at + meta.size * elem);
449 bytes.filter(|_| meta.flags.row_contiguous)
450 });
451 if let Some(bytes) = bytes {
452 return eval_bytes(op, comm, meta.dtype, &meta.shape, Some(bytes), out);
453 }
454 let input = crate::kernels::contiguous(meta, data)?;
455 eval_bytes(op, comm, meta.dtype, &meta.shape, Some(input.bytes()), out)
456}
457
458pub(crate) fn eval_cpu(
461 op: &CollectiveOp,
462 group: usize,
463 operands: &[Operand<'_>],
464) -> Result<Vec<Data>> {
465 let entry = registered(group)?;
466 let mut out = Buffer::try_alloc(output_len(op, entry.comm.size(), operands))?;
467 let _order = entry.worker.idle_order(&entry.order);
468 eval_into(op, &*entry.comm, operands, out.bytes_mut())?;
469 Ok(vec![Data::new(out)])
470}
471
472#[cfg(all(target_os = "macos", not(miri)))]
474pub(crate) enum Ready {
475 Now,
477 Signal(Arc<crate::gpu::BufferSignal>, u64),
479 Pending(Option<nuvec_gpu::Pending>),
481}
482
483#[cfg(all(target_os = "macos", not(miri)))]
485pub(crate) enum Input {
486 InOutput,
489 Staged(Data),
491 Operand(Array, Data),
493}
494
495#[cfg(all(target_os = "macos", not(miri)))]
497pub(crate) struct GpuJob {
498 pub(crate) op: CollectiveOp,
499 pub(crate) dtype: DType,
500 pub(crate) shape: Vec<usize>,
501 pub(crate) ready: Ready,
502 pub(crate) input: Input,
503 pub(crate) out: Data,
504 pub(crate) status: crate::array::ChunkStatus,
505}
506
507#[cfg(all(target_os = "macos", not(miri)))]
512pub(crate) fn submit_gpu(
513 group: usize,
514 job: GpuJob,
515 device: &nuvec_gpu::Device,
516) -> Result<(Arc<dyn std::any::Any + Send + Sync>, u64)> {
517 let entry = registered(group)?;
518 entry.worker.event(device)?;
519 let worker = entry.worker.clone();
520 let value = worker.submit(move |value| {
521 let GpuJob {
522 op,
523 dtype,
524 shape,
525 ready,
526 input,
527 out,
528 status,
529 } = job;
530 let run = || {
531 let ready = match ready {
532 Ready::Now => Ok(()),
533 Ready::Signal(signal, v) => signal.wait(v),
534 Ready::Pending(p) => p.map_or(Ok(()), |p| {
535 p.wait_result().map_err(|e| Error::Backend(e.to_string()))
536 }),
537 };
538 let _order = entry.order.lock().unwrap_or_else(PoisonError::into_inner);
539 let ran = write_unsynchronized(&out, |bytes| match &input {
540 Input::InOutput => eval_bytes(&op, &*entry.comm, dtype, &shape, None, bytes),
541 Input::Staged(staged) => read_unsynchronized(staged, |i| {
542 eval_bytes(&op, &*entry.comm, dtype, &shape, Some(i), bytes)
543 }),
544 Input::Operand(a, d) => eval_into(&op, &*entry.comm, &[(a.out(), d)], bytes),
545 });
546 ready.and(ran)
547 };
548 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run))
549 .unwrap_or_else(|_| Err(Error::Backend("distributed: a collective panicked".into())));
550 if let Err(e) = result {
551 let _ = status.set(e);
552 }
553 if let Some(event) = entry.worker.event.get() {
554 event.signal(value);
555 }
556 drop((input, out));
557 });
558 Ok((worker, value))
559}
560
561#[cfg(all(target_os = "macos", not(miri)))]
563pub(crate) fn worker_event(worker: &(dyn std::any::Any + Send + Sync)) -> &nuvec_gpu::Event {
564 worker
565 .downcast_ref::<worker::Worker>()
566 .and_then(|w| w.event.get())
567 .expect("created by submit_gpu")
568}
569
570#[cfg(all(target_os = "macos", not(miri)))]
573#[allow(unsafe_code)]
574fn read_unsynchronized(staged: &Data, f: impl FnOnce(&[u8]) -> Result<()>) -> Result<()> {
575 let len = staged.buffer.len();
576 let Some(metal) = staged.buffer.metal() else {
577 return f(staged.buffer.bytes());
578 };
579 let (ptr, available) = metal
580 .unsynchronized_ptr()
581 .map_err(|e| Error::Backend(e.to_string()))?;
582 assert!(len <= available, "the staging buffer holds its bytes");
583 f(unsafe { std::slice::from_raw_parts(ptr.as_ptr(), len) })
587}
588
589#[cfg(all(target_os = "macos", not(miri)))]
591#[allow(unsafe_code)]
592fn write_unsynchronized(out: &Data, f: impl FnOnce(&mut [u8]) -> Result<()>) -> Result<()> {
593 let len = out.buffer.len();
594 let Some(metal) = out.buffer.metal() else {
595 return Err(Error::Backend(
596 "distributed: a GPU collective's output is not on the GPU".into(),
597 ));
598 };
599 let (ptr, available) = metal
600 .unsynchronized_ptr()
601 .map_err(|e| Error::Backend(e.to_string()))?;
602 assert!(len <= available, "the output buffer holds its bytes");
603 let bytes = unsafe { std::slice::from_raw_parts_mut(ptr.as_ptr(), len) };
608 f(bytes)
609}
610
611#[track_caller]
622pub fn all_sum(x: &Array, group: Option<&Group>) -> Array {
623 Array::sticky("all_sum", Location::caller(), &[x], || {
624 reduce(x, AllReduce::Sum, group)
625 })
626}
627
628#[track_caller]
630pub fn all_max(x: &Array, group: Option<&Group>) -> Array {
631 Array::sticky("all_max", Location::caller(), &[x], || {
632 reduce(x, AllReduce::Max, group)
633 })
634}
635
636#[track_caller]
638pub fn all_min(x: &Array, group: Option<&Group>) -> Array {
639 Array::sticky("all_min", Location::caller(), &[x], || {
640 reduce(x, AllReduce::Min, group)
641 })
642}
643
644#[track_caller]
647pub fn all_gather(x: &Array, group: Option<&Group>) -> Array {
648 Array::sticky("all_gather", Location::caller(), &[x], || gather(x, group))
649}
650
651#[track_caller]
663pub fn sum_scatter(x: &Array, group: Option<&Group>) -> Array {
664 Array::sticky("sum_scatter", Location::caller(), &[x], || {
665 scatter(x, group)
666 })
667}
668
669#[track_caller]
672pub fn send(x: &Array, dst: usize, group: Option<&Group>) -> Array {
673 Array::sticky("send", Location::caller(), &[x], || send_op(x, dst, group))
674}
675
676#[track_caller]
679pub fn recv(shape: impl IntoShape, dtype: DType, src: usize, group: Option<&Group>) -> Array {
680 let spec = shape.shape_spec();
681 Array::constructor("recv", Location::caller(), dtype, &spec, |shape| {
682 recv_op(shape.to_vec(), dtype, src, group)
683 })
684}
685
686#[track_caller]
688pub fn recv_like(x: &Array, src: usize, group: Option<&Group>) -> Array {
689 let at = Location::caller();
690 Array::sticky("recv_like", at, &[x], || {
691 recv_op(x.dims().to_vec(), x.dtype(), src, group)
692 })
693}
694
695#[track_caller]
699pub fn sum_gradients(x: &Array, group: Option<&Group>) -> Array {
700 let group = group_or_default(group);
701 if group.size == 1 {
702 return x.clone();
703 }
704 Array::sticky("sum_gradients", Location::caller(), &[x], || {
705 let f = crate::custom_function_flat(|xs| Ok(xs.to_vec())).with_vjp(
706 move |_primals, _outputs, cotangents| {
707 cotangents
708 .iter()
709 .map(|c| reduce(c, AllReduce::Sum, Some(&group)))
710 .collect()
711 },
712 );
713 Ok(f.call(std::slice::from_ref(x))?.remove(0))
714 })
715}
716
717impl crate::tree::Static for Group {
718 fn hash_static(&self, mut h: &mut dyn std::hash::Hasher) {
719 std::hash::Hash::hash(&(self.id, self.rank, self.size), &mut h);
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use std::sync::Mutex;
726 use std::time::Duration;
727
728 use super::*;
729
730 struct Fake {
733 log: Mutex<Vec<String>>,
734 fail_at: Option<usize>,
735 }
736
737 impl Communicator for Fake {
738 fn rank(&self) -> usize {
739 0
740 }
741 fn size(&self) -> usize {
742 2
743 }
744 fn all_reduce(&self, op: AllReduce, dtype: DType, data: &mut [u8]) -> Result<()> {
745 std::thread::sleep(Duration::from_millis(2));
746 let n = data.len() / dtype.size();
747 self.log.lock().unwrap().push(format!("reduce {n}"));
748 if self.fail_at == Some(n) {
749 return Err(Error::Backend("distributed: rank 1 left".into()));
750 }
751 if op == AllReduce::Sum && dtype == DType::F32 {
752 for v in bytemuck::cast_slice_mut::<u8, f32>(data) {
753 *v *= 2.0;
754 }
755 }
756 Ok(())
757 }
758 fn all_gather(&self, input: &[u8], output: &mut [u8]) -> Result<()> {
759 output[..input.len()].copy_from_slice(input);
760 output[input.len()..].copy_from_slice(input);
761 Ok(())
762 }
763 fn send(&self, _: usize, _: &[u8]) -> Result<()> {
764 Ok(())
765 }
766 fn recv(&self, _: usize, data: &mut [u8]) -> Result<()> {
767 data.fill(0);
768 Ok(())
769 }
770 }
771
772 fn fake(fail_at: Option<usize>) -> (Group, Arc<Fake>) {
773 let comm = Arc::new(Fake {
774 log: Mutex::new(Vec::new()),
775 fail_at,
776 });
777 (Group::new(comm.clone()), comm)
778 }
779
780 fn gpu() -> bool {
781 if !crate::gpu::available() {
782 return false;
783 }
784 crate::set_default_stream(crate::Stream::default_for(crate::Device::gpu(0)));
785 true
786 }
787
788 #[test]
791 fn collectives_run_in_program_order_across_streams() {
792 if !gpu() {
793 return;
794 }
795 let (g, comm) = fake(None);
796 let x = |n: usize| crate::ops::ones([n as i32], DType::F32);
797 let a = all_sum(&x(1).multiply(3.0), Some(&g));
798 let b = all_sum(&x(2).multiply(3.0), Some(&g));
799 crate::async_eval(&a).unwrap();
800 crate::async_eval(&b).unwrap();
801 crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
802 let c = all_sum(&x(3), Some(&g));
803 assert_eq!(c.to_vec::<f32>().unwrap(), [2.0; 3]);
804 assert_eq!(a.to_vec::<f32>().unwrap(), [6.0]);
805 assert_eq!(b.to_vec::<f32>().unwrap(), [6.0; 2]);
806 assert_eq!(
807 *comm.log.lock().unwrap(),
808 ["reduce 1", "reduce 2", "reduce 3"]
809 );
810 }
811
812 #[test]
814 fn a_dropped_result_still_runs() {
815 if !gpu() {
816 return;
817 }
818 let (g, comm) = fake(None);
819 let a = all_sum(&crate::ops::ones([4], DType::F32).multiply(2.0), Some(&g));
820 crate::async_eval(&a).unwrap();
821 drop(a);
822 let b = all_sum(&crate::ops::ones([5], DType::F32).add(1.0), Some(&g));
823 assert_eq!(b.to_vec::<f32>().unwrap(), [4.0; 5]);
824 assert_eq!(*comm.log.lock().unwrap(), ["reduce 4", "reduce 5"]);
825 }
826
827 #[test]
830 fn a_failed_collective_fails_what_follows() {
831 if !gpu() {
832 return;
833 }
834 let (g, _) = fake(Some(7));
835 let y = all_sum(&crate::ops::ones([7], DType::F32).multiply(2.0), Some(&g)).add(1.0);
836 let err = y.eval().unwrap_err().to_string();
837 assert!(err.contains("rank 1 left"), "{err}");
838 }
839
840 #[cfg(all(target_os = "macos", not(miri)))]
842 #[test]
843 fn a_gpu_failure_before_a_collective_fails_it() {
844 if !gpu() {
845 return;
846 }
847 let (g, comm) = fake(None);
848 let x = crate::ops::ones([6], DType::F32).multiply(2.0);
849 crate::gpu::fault::fail_next_commit();
850 let y = all_sum(&x, Some(&g)).add(1.0);
851 assert!(y.eval().is_err());
852 assert_eq!(*comm.log.lock().unwrap(), ["reduce 6"]);
853 }
854
855 #[cfg(all(target_os = "macos", not(miri)))]
858 #[test]
859 fn collectives_do_not_commit_command_buffers() {
860 if !gpu() {
861 return;
862 }
863 let (g, _) = fake(None);
864 let x = crate::ops::ones([1, 1024], DType::F32);
865 x.eval().unwrap();
866 let chain = |x: &Array, collectives: bool| {
867 let mut y = x.clone();
868 for _ in 0..56 {
869 y = y.multiply(0.5);
870 if collectives {
871 y = all_sum(&y, Some(&g));
872 }
873 }
874 y
875 };
876 chain(&x, true).eval().unwrap();
878 chain(&x, false).eval().unwrap();
879 let count = |collectives: bool| {
880 let before = crate::gpu::COMMITS.get();
881 let y = chain(&x, collectives);
882 y.eval().unwrap();
883 (crate::gpu::COMMITS.get() - before, y)
884 };
885 let (with, y) = count(true);
886 let (without, _) = count(false);
887 assert!(
888 with <= without + 56 / 8,
889 "{with} command buffers for 56 collectives, {without} without them"
890 );
891 assert_eq!(y.to_vec::<f32>().unwrap(), vec![1.0; 1024]);
892 }
893
894 #[test]
897 fn a_cpu_collective_after_a_gpu_one_in_one_evaluation() {
898 if !gpu() {
899 return;
900 }
901 let (g, comm) = fake(None);
902 let a = all_sum(&crate::ops::ones([3], DType::F32).multiply(3.0), Some(&g));
903 crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
904 let c = all_sum(&a.add(1.0), Some(&g));
905 assert_eq!(c.to_vec::<f32>().unwrap(), [14.0; 3]);
906 assert_eq!(*comm.log.lock().unwrap(), ["reduce 3", "reduce 3"]);
907 }
908
909 #[test]
913 fn only_inputs_nothing_else_holds_are_reduced_in_place() {
914 if !gpu() {
915 return;
916 }
917 let (g, _comm) = fake(None);
918 let held = crate::ops::ones([5], DType::F32).multiply(3.0);
919 held.eval().unwrap();
920 let sum = all_sum(&held, Some(&g));
921 assert_eq!(sum.to_vec::<f32>().unwrap(), [6.0; 5]);
922 assert_eq!(held.to_vec::<f32>().unwrap(), [3.0; 5]);
923
924 let intermediate = all_sum(&crate::ops::ones([5], DType::F32).multiply(2.0), Some(&g));
925 assert_eq!(intermediate.to_vec::<f32>().unwrap(), [4.0; 5]);
926
927 let done = crate::ops::ones([5], DType::F32).multiply(5.0);
928 done.eval().unwrap();
929 let sum = all_sum(&done, Some(&g));
930 drop(done);
931 assert_eq!(sum.to_vec::<f32>().unwrap(), [10.0; 5]);
932
933 let x = crate::ops::ones([5], DType::F32).multiply(4.0);
935 let (sum, other) = (all_sum(&x, Some(&g)), x.add(1.0));
936 drop(x);
937 crate::eval(&[sum.clone(), other.clone()]).unwrap();
938 assert_eq!(sum.to_vec::<f32>().unwrap(), [8.0; 5]);
939 assert_eq!(other.to_vec::<f32>().unwrap(), [5.0; 5]);
940 }
941
942 #[cfg(all(target_os = "macos", not(miri)))]
944 #[test]
945 fn a_failed_buffer_fails_every_collective_in_it() {
946 if !gpu() {
947 return;
948 }
949 let (g, comm) = fake(None);
950 let mut y = crate::ops::ones([8], DType::F32);
951 for _ in 0..3 {
952 y = all_sum(&y.multiply(2.0), Some(&g));
953 }
954 crate::gpu::fault::fail_next_commit();
955 assert!(y.eval().is_err());
956 assert_eq!(comm.log.lock().unwrap().len(), 3);
957 }
958}