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 entry.worker.finish(value);
556 if let Some(event) = entry.worker.event.get() {
557 event.signal(value);
558 }
559 drop((input, out));
560 });
561 Ok((worker, value))
562}
563
564#[cfg(all(target_os = "macos", not(miri)))]
567pub(crate) fn after_gpu(
568 worker: &(dyn std::any::Any + Send + Sync),
569 value: u64,
570 f: impl FnOnce() + Send + 'static,
571) {
572 worker
573 .downcast_ref::<worker::Worker>()
574 .expect("returned by submit_gpu")
575 .after(value, f);
576}
577
578#[cfg(all(target_os = "macos", not(miri)))]
580pub(crate) fn worker_event(worker: &(dyn std::any::Any + Send + Sync)) -> &nuvec_gpu::Event {
581 worker
582 .downcast_ref::<worker::Worker>()
583 .and_then(|w| w.event.get())
584 .expect("created by submit_gpu")
585}
586
587#[cfg(all(target_os = "macos", not(miri)))]
590#[allow(unsafe_code)]
591fn read_unsynchronized(staged: &Data, f: impl FnOnce(&[u8]) -> Result<()>) -> Result<()> {
592 let len = staged.buffer.len();
593 let Some(metal) = staged.buffer.metal() else {
594 return f(staged.buffer.bytes());
595 };
596 let (ptr, available) = metal
597 .unsynchronized_ptr()
598 .map_err(|e| Error::Backend(e.to_string()))?;
599 assert!(len <= available, "the staging buffer holds its bytes");
600 f(unsafe { std::slice::from_raw_parts(ptr.as_ptr(), len) })
604}
605
606#[cfg(all(target_os = "macos", not(miri)))]
608#[allow(unsafe_code)]
609fn write_unsynchronized(out: &Data, f: impl FnOnce(&mut [u8]) -> Result<()>) -> Result<()> {
610 let len = out.buffer.len();
611 let Some(metal) = out.buffer.metal() else {
612 return Err(Error::Backend(
613 "distributed: a GPU collective's output is not on the GPU".into(),
614 ));
615 };
616 let (ptr, available) = metal
617 .unsynchronized_ptr()
618 .map_err(|e| Error::Backend(e.to_string()))?;
619 assert!(len <= available, "the output buffer holds its bytes");
620 let bytes = unsafe { std::slice::from_raw_parts_mut(ptr.as_ptr(), len) };
625 f(bytes)
626}
627
628#[track_caller]
639pub fn all_sum(x: &Array, group: Option<&Group>) -> Array {
640 Array::sticky("all_sum", Location::caller(), &[x], || {
641 reduce(x, AllReduce::Sum, group)
642 })
643}
644
645#[track_caller]
647pub fn all_max(x: &Array, group: Option<&Group>) -> Array {
648 Array::sticky("all_max", Location::caller(), &[x], || {
649 reduce(x, AllReduce::Max, group)
650 })
651}
652
653#[track_caller]
655pub fn all_min(x: &Array, group: Option<&Group>) -> Array {
656 Array::sticky("all_min", Location::caller(), &[x], || {
657 reduce(x, AllReduce::Min, group)
658 })
659}
660
661#[track_caller]
664pub fn all_gather(x: &Array, group: Option<&Group>) -> Array {
665 Array::sticky("all_gather", Location::caller(), &[x], || gather(x, group))
666}
667
668#[track_caller]
680pub fn sum_scatter(x: &Array, group: Option<&Group>) -> Array {
681 Array::sticky("sum_scatter", Location::caller(), &[x], || {
682 scatter(x, group)
683 })
684}
685
686#[track_caller]
689pub fn send(x: &Array, dst: usize, group: Option<&Group>) -> Array {
690 Array::sticky("send", Location::caller(), &[x], || send_op(x, dst, group))
691}
692
693#[track_caller]
696pub fn recv(shape: impl IntoShape, dtype: DType, src: usize, group: Option<&Group>) -> Array {
697 let spec = shape.shape_spec();
698 Array::constructor("recv", Location::caller(), dtype, &spec, |shape| {
699 recv_op(shape.to_vec(), dtype, src, group)
700 })
701}
702
703#[track_caller]
705pub fn recv_like(x: &Array, src: usize, group: Option<&Group>) -> Array {
706 let at = Location::caller();
707 Array::sticky("recv_like", at, &[x], || {
708 recv_op(x.dims().to_vec(), x.dtype(), src, group)
709 })
710}
711
712#[track_caller]
716pub fn sum_gradients(x: &Array, group: Option<&Group>) -> Array {
717 let group = group_or_default(group);
718 if group.size == 1 {
719 return x.clone();
720 }
721 Array::sticky("sum_gradients", Location::caller(), &[x], || {
722 let f = crate::custom_function_flat(|xs| Ok(xs.to_vec())).with_vjp(
723 move |_primals, _outputs, cotangents| {
724 cotangents
725 .iter()
726 .map(|c| reduce(c, AllReduce::Sum, Some(&group)))
727 .collect()
728 },
729 );
730 Ok(f.call(std::slice::from_ref(x))?.remove(0))
731 })
732}
733
734impl crate::tree::Static for Group {
735 fn hash_static(&self, mut h: &mut dyn std::hash::Hasher) {
736 std::hash::Hash::hash(&(self.id, self.rank, self.size), &mut h);
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use std::sync::Mutex;
743 use std::time::Duration;
744
745 use super::*;
746
747 struct Fake {
751 log: Mutex<Vec<String>>,
752 fail_at: Option<usize>,
753 delay: Duration,
754 }
755
756 impl Communicator for Fake {
757 fn rank(&self) -> usize {
758 0
759 }
760 fn size(&self) -> usize {
761 2
762 }
763 fn all_reduce(&self, op: AllReduce, dtype: DType, data: &mut [u8]) -> Result<()> {
764 std::thread::sleep(self.delay);
765 let n = data.len() / dtype.size();
766 self.log.lock().unwrap().push(format!("reduce {n}"));
767 if self.fail_at == Some(n) {
768 return Err(Error::Backend("distributed: rank 1 left".into()));
769 }
770 if op == AllReduce::Sum && dtype == DType::F32 {
771 for v in bytemuck::cast_slice_mut::<u8, f32>(data) {
772 *v *= 2.0;
773 }
774 }
775 Ok(())
776 }
777 fn all_gather(&self, input: &[u8], output: &mut [u8]) -> Result<()> {
778 output[..input.len()].copy_from_slice(input);
779 output[input.len()..].copy_from_slice(input);
780 Ok(())
781 }
782 fn send(&self, _: usize, _: &[u8]) -> Result<()> {
783 Ok(())
784 }
785 fn recv(&self, _: usize, data: &mut [u8]) -> Result<()> {
786 data.fill(0);
787 Ok(())
788 }
789 }
790
791 fn fake(fail_at: Option<usize>) -> (Group, Arc<Fake>) {
792 slow(fail_at, Duration::from_millis(2))
793 }
794
795 fn slow(fail_at: Option<usize>, delay: Duration) -> (Group, Arc<Fake>) {
796 let comm = Arc::new(Fake {
797 log: Mutex::new(Vec::new()),
798 fail_at,
799 delay,
800 });
801 (Group::new(comm.clone()), comm)
802 }
803
804 fn gpu() -> bool {
805 if !crate::gpu::available() {
806 return false;
807 }
808 crate::set_default_stream(crate::Stream::default_for(crate::Device::gpu(0)));
809 true
810 }
811
812 #[test]
815 fn collectives_run_in_program_order_across_streams() {
816 if !gpu() {
817 return;
818 }
819 let (g, comm) = fake(None);
820 let x = |n: usize| crate::ops::ones([n as i32], DType::F32);
821 let a = all_sum(&x(1).multiply(3.0), Some(&g));
822 let b = all_sum(&x(2).multiply(3.0), Some(&g));
823 crate::async_eval(&a).unwrap();
824 crate::async_eval(&b).unwrap();
825 crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
826 let c = all_sum(&x(3), Some(&g));
827 assert_eq!(c.to_vec::<f32>().unwrap(), [2.0; 3]);
828 assert_eq!(a.to_vec::<f32>().unwrap(), [6.0]);
829 assert_eq!(b.to_vec::<f32>().unwrap(), [6.0; 2]);
830 assert_eq!(
831 *comm.log.lock().unwrap(),
832 ["reduce 1", "reduce 2", "reduce 3"]
833 );
834 }
835
836 #[test]
838 fn a_dropped_result_still_runs() {
839 if !gpu() {
840 return;
841 }
842 let (g, comm) = fake(None);
843 let a = all_sum(&crate::ops::ones([4], DType::F32).multiply(2.0), Some(&g));
844 crate::async_eval(&a).unwrap();
845 drop(a);
846 let b = all_sum(&crate::ops::ones([5], DType::F32).add(1.0), Some(&g));
847 assert_eq!(b.to_vec::<f32>().unwrap(), [4.0; 5]);
848 assert_eq!(*comm.log.lock().unwrap(), ["reduce 4", "reduce 5"]);
849 }
850
851 #[test]
854 fn a_failed_collective_fails_what_follows() {
855 if !gpu() {
856 return;
857 }
858 let (g, _) = fake(Some(7));
859 let y = all_sum(&crate::ops::ones([7], DType::F32).multiply(2.0), Some(&g)).add(1.0);
860 let err = y.eval().unwrap_err().to_string();
861 assert!(err.contains("rank 1 left"), "{err}");
862 }
863
864 #[cfg(all(target_os = "macos", not(miri)))]
866 #[test]
867 fn a_gpu_failure_before_a_collective_fails_it() {
868 if !gpu() {
869 return;
870 }
871 let (g, comm) = fake(None);
872 let x = crate::ops::ones([6], DType::F32).multiply(2.0);
873 crate::gpu::fault::fail_next_commit();
874 let y = all_sum(&x, Some(&g)).add(1.0);
875 assert!(y.eval().is_err());
876 assert_eq!(*comm.log.lock().unwrap(), ["reduce 6"]);
877 }
878
879 #[cfg(all(target_os = "macos", not(miri)))]
883 #[test]
884 fn collectives_commit_one_command_buffer_each() {
885 if !gpu() {
886 return;
887 }
888 let (g, _) = fake(None);
889 let x = crate::ops::ones([1, 1024], DType::F32);
890 x.eval().unwrap();
891 let chain = |x: &Array, collectives: bool| {
892 let mut y = x.clone();
893 for _ in 0..56 {
894 y = y.multiply(0.5);
895 if collectives {
896 y = all_sum(&y, Some(&g));
897 }
898 }
899 y
900 };
901 chain(&x, true).eval().unwrap();
903 chain(&x, false).eval().unwrap();
904 let count = |collectives: bool| {
905 let before = crate::gpu::COMMITS.get();
906 let y = chain(&x, collectives);
907 y.eval().unwrap();
908 (crate::gpu::COMMITS.get() - before, y)
909 };
910 let (with, y) = count(true);
911 let (without, _) = count(false);
912 assert!(
913 with <= without + 56,
914 "{with} command buffers for 56 collectives, {without} without them"
915 );
916 assert_eq!(y.to_vec::<f32>().unwrap(), vec![1.0; 1024]);
917 }
918
919 #[cfg(all(target_os = "macos", not(miri)))]
922 #[test]
923 fn a_collective_slower_than_the_gpu_timeout() {
924 if !gpu() {
925 return;
926 }
927 let (g, _) = slow(None, Duration::from_secs(6));
928 let x = crate::ops::ones([4], DType::F32).multiply(2.0);
929 let y = all_sum(&x, Some(&g)).add(1.0).multiply(2.0);
930 assert_eq!(y.to_vec::<f32>().unwrap(), [10.0; 4]);
931 let z = crate::ops::ones([4], DType::F32).add(2.0);
932 assert_eq!(z.to_vec::<f32>().unwrap(), [3.0; 4]);
933 }
934
935 #[test]
938 fn a_cpu_collective_after_a_gpu_one_in_one_evaluation() {
939 if !gpu() {
940 return;
941 }
942 let (g, comm) = fake(None);
943 let a = all_sum(&crate::ops::ones([3], DType::F32).multiply(3.0), Some(&g));
944 crate::set_default_stream(crate::Stream::default_for(crate::Device::cpu()));
945 let c = all_sum(&a.add(1.0), Some(&g));
946 assert_eq!(c.to_vec::<f32>().unwrap(), [14.0; 3]);
947 assert_eq!(*comm.log.lock().unwrap(), ["reduce 3", "reduce 3"]);
948 }
949
950 #[test]
954 fn only_inputs_nothing_else_holds_are_reduced_in_place() {
955 if !gpu() {
956 return;
957 }
958 let (g, _comm) = fake(None);
959 let held = crate::ops::ones([5], DType::F32).multiply(3.0);
960 held.eval().unwrap();
961 let sum = all_sum(&held, Some(&g));
962 assert_eq!(sum.to_vec::<f32>().unwrap(), [6.0; 5]);
963 assert_eq!(held.to_vec::<f32>().unwrap(), [3.0; 5]);
964
965 let intermediate = all_sum(&crate::ops::ones([5], DType::F32).multiply(2.0), Some(&g));
966 assert_eq!(intermediate.to_vec::<f32>().unwrap(), [4.0; 5]);
967
968 let done = crate::ops::ones([5], DType::F32).multiply(5.0);
969 done.eval().unwrap();
970 let sum = all_sum(&done, Some(&g));
971 drop(done);
972 assert_eq!(sum.to_vec::<f32>().unwrap(), [10.0; 5]);
973
974 let x = crate::ops::ones([5], DType::F32).multiply(4.0);
976 let (sum, other) = (all_sum(&x, Some(&g)), x.add(1.0));
977 drop(x);
978 crate::eval(&[sum.clone(), other.clone()]).unwrap();
979 assert_eq!(sum.to_vec::<f32>().unwrap(), [8.0; 5]);
980 assert_eq!(other.to_vec::<f32>().unwrap(), [5.0; 5]);
981 }
982
983 #[cfg(all(target_os = "macos", not(miri)))]
985 #[test]
986 fn a_failed_buffer_fails_the_collectives_after_it() {
987 if !gpu() {
988 return;
989 }
990 let (g, comm) = fake(None);
991 let mut y = crate::ops::ones([8], DType::F32);
992 for _ in 0..3 {
993 y = all_sum(&y.multiply(2.0), Some(&g));
994 }
995 crate::gpu::fault::fail_next_commit();
996 assert!(y.eval().is_err());
997 assert_eq!(comm.log.lock().unwrap().len(), 3);
998 }
999}