1use crate::collective::ReduceKind;
33use crate::symmetric::CollectiveError;
34use half::slice::HalfFloatSliceExt;
35use rlx_ir::DType;
36use std::sync::Arc;
37use std::time::{Duration, Instant};
38
39pub const TAG_RESERVED_BASE: u32 = 0xFFF0_0000;
43const TAG_BARRIER: u32 = TAG_RESERVED_BASE;
44const TAG_ALL_REDUCE: u32 = TAG_RESERVED_BASE + 1;
45const TAG_ALL_GATHER: u32 = TAG_RESERVED_BASE + 2;
46const TAG_BROADCAST: u32 = TAG_RESERVED_BASE + 3;
47const TAG_ALL_TO_ALL: u32 = TAG_RESERVED_BASE + 4;
48const TAG_REDUCE: u32 = TAG_RESERVED_BASE + 5;
49const TAG_PPERMUTE: u32 = TAG_RESERVED_BASE + 6;
50const TAG_ALL_TO_ALL_V: u32 = TAG_RESERVED_BASE + 7;
53
54pub trait Transport: Send + Sync {
63 fn rank(&self) -> u32;
65 fn world_size(&self) -> u32;
67
68 fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError>;
71
72 fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError>;
75
76 fn recv_bytes_timeout(
82 &self,
83 from: u32,
84 tag: u32,
85 _timeout: Duration,
86 ) -> Result<Option<Vec<u8>>, CollectiveError> {
87 Ok(Some(self.recv_bytes(from, tag)?))
88 }
89
90 fn barrier(&self) -> Result<(), CollectiveError> {
98 default_barrier(self)
99 }
100}
101
102pub fn default_barrier<T: Transport + ?Sized>(t: &T) -> Result<(), CollectiveError> {
106 let n = t.world_size();
107 if n <= 1 {
108 return Ok(());
109 }
110 let me = t.rank();
111 if me == 0 {
112 for r in 1..n {
113 t.recv_bytes(r, TAG_BARRIER)?;
114 }
115 for r in 1..n {
116 t.send_bytes(r, TAG_BARRIER, &[1u8])?;
117 }
118 } else {
119 t.send_bytes(0, TAG_BARRIER, &[1u8])?;
120 t.recv_bytes(0, TAG_BARRIER)?;
121 }
122 Ok(())
123}
124
125fn f32_to_le_bytes(data: &[f32]) -> Vec<u8> {
129 let mut out = Vec::with_capacity(data.len() * 4);
130 for &x in data {
131 out.extend_from_slice(&x.to_le_bytes());
132 }
133 out
134}
135
136fn le_bytes_to_f32(bytes: &[u8]) -> Result<Vec<f32>, CollectiveError> {
139 if !bytes.len().is_multiple_of(4) {
140 return Err(CollectiveError::TransportError {
141 reason: format!("recv payload {} bytes is not a multiple of 4", bytes.len()),
142 });
143 }
144 Ok(bytes
145 .chunks_exact(4)
146 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
147 .collect())
148}
149
150fn combine(op: ReduceKind, a: f32, b: f32) -> f32 {
151 match op {
152 ReduceKind::Sum | ReduceKind::Mean => a + b,
153 ReduceKind::Max => a.max(b),
154 ReduceKind::Min => a.min(b),
155 }
156}
157
158fn finalize(op: ReduceKind, acc: f32, n: u32) -> f32 {
159 match op {
160 ReduceKind::Mean => acc / (n as f32),
161 _ => acc,
162 }
163}
164
165fn finalize64(op: ReduceKind, acc: f64, n: u32) -> f64 {
167 match op {
168 ReduceKind::Mean => acc / (n as f64),
169 _ => acc,
170 }
171}
172
173fn ring_chunk(len: usize, n: usize, i: usize) -> (usize, usize) {
179 let base = len / n;
180 let rem = len % n;
181 let bound = |j: usize| j * base + j.min(rem);
182 (bound(i), bound(i + 1))
183}
184
185fn expect_len(got: usize, want: usize) -> Result<(), CollectiveError> {
187 if got != want {
188 return Err(CollectiveError::LengthMismatch {
189 expected: want,
190 got,
191 });
192 }
193 Ok(())
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum ReduceMode {
200 Ring,
203 Deterministic,
214}
215
216pub fn env_reduce_mode() -> ReduceMode {
224 static MODE: std::sync::OnceLock<ReduceMode> = std::sync::OnceLock::new();
225 *MODE.get_or_init(|| {
226 let on = |k: &str| {
227 std::env::var(k)
228 .map(|v| v != "0" && !v.is_empty())
229 .unwrap_or(false)
230 };
231 if on("RLX_DETERMINISTIC_REDUCE") {
232 ReduceMode::Deterministic
233 } else {
234 ReduceMode::Ring
235 }
236 })
237}
238
239fn combine64(op: ReduceKind, a: f64, b: f64) -> f64 {
242 match op {
243 ReduceKind::Sum | ReduceKind::Mean => a + b,
244 ReduceKind::Max => a.max(b),
245 ReduceKind::Min => a.min(b),
246 }
247}
248
249fn is_reducible_float(d: DType) -> bool {
251 matches!(d, DType::F16 | DType::BF16 | DType::F32 | DType::F64)
252}
253
254fn is_reducible_int(d: DType) -> bool {
257 matches!(d, DType::I8 | DType::U8)
258}
259
260fn decode_to_i32(bytes: &[u8], dtype: DType) -> Vec<i32> {
261 match dtype {
262 DType::I8 => bytes.iter().map(|&b| b as i8 as i32).collect(),
263 DType::U8 => bytes.iter().map(|&b| b as i32).collect(),
264 _ => Vec::new(),
265 }
266}
267
268fn encode_into_i32(dst: &mut [u8], vals: &[i32], dtype: DType) {
269 match dtype {
270 DType::I8 => {
271 for (d, &v) in dst.iter_mut().zip(vals) {
272 *d = v.clamp(i8::MIN as i32, i8::MAX as i32) as i8 as u8;
273 }
274 }
275 DType::U8 => {
276 for (d, &v) in dst.iter_mut().zip(vals) {
277 *d = v.clamp(0, u8::MAX as i32) as u8;
278 }
279 }
280 _ => {}
281 }
282}
283
284fn combine_i32(op: ReduceKind, a: i32, b: i32) -> i32 {
285 match op {
286 ReduceKind::Sum | ReduceKind::Mean => a + b,
287 ReduceKind::Max => a.max(b),
288 ReduceKind::Min => a.min(b),
289 }
290}
291
292fn decode_to_f64(bytes: &[u8], dtype: DType) -> Vec<f64> {
294 match dtype {
295 DType::F32 => bytes
296 .chunks_exact(4)
297 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f64)
298 .collect(),
299 DType::F64 => bytes
300 .chunks_exact(8)
301 .map(|c| f64::from_le_bytes(c.try_into().unwrap()))
302 .collect(),
303 DType::F16 => bytes
304 .chunks_exact(2)
305 .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f64())
306 .collect(),
307 DType::BF16 => bytes
308 .chunks_exact(2)
309 .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f64())
310 .collect(),
311 _ => Vec::new(),
312 }
313}
314
315fn encode_from_f64(vals: &[f64], dtype: DType) -> Vec<u8> {
317 let mut out = Vec::with_capacity(vals.len() * dtype.size_bytes().max(1));
318 match dtype {
319 DType::F32 => vals
320 .iter()
321 .for_each(|&v| out.extend_from_slice(&(v as f32).to_le_bytes())),
322 DType::F64 => vals
323 .iter()
324 .for_each(|&v| out.extend_from_slice(&v.to_le_bytes())),
325 DType::F16 => vals
326 .iter()
327 .for_each(|&v| out.extend_from_slice(&half::f16::from_f64(v).to_le_bytes())),
328 DType::BF16 => vals
329 .iter()
330 .for_each(|&v| out.extend_from_slice(&half::bf16::from_f64(v).to_le_bytes())),
331 _ => {}
332 }
333 out
334}
335
336fn combine32(op: ReduceKind, a: f32, b: f32) -> f32 {
339 match op {
340 ReduceKind::Sum | ReduceKind::Mean => a + b,
341 ReduceKind::Max => a.max(b),
342 ReduceKind::Min => a.min(b),
343 }
344}
345
346fn decode_to_f32(bytes: &[u8], dtype: DType) -> Vec<f32> {
352 match dtype {
353 DType::F32 => {
354 #[cfg(target_endian = "little")]
355 {
356 let (head, mid, tail) = unsafe { bytes.align_to::<f32>() };
357 if head.is_empty() && tail.is_empty() {
358 return mid.to_vec();
359 }
360 }
361 bytes
362 .chunks_exact(4)
363 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
364 .collect()
365 }
366 DType::F16 => {
367 let mut out = vec![0f32; bytes.len() / 2];
368 #[cfg(target_endian = "little")]
369 {
370 let (head, mid, tail) = unsafe { bytes.align_to::<half::f16>() };
373 if head.is_empty() && tail.is_empty() {
374 mid.convert_to_f32_slice(&mut out);
375 return out;
376 }
377 }
378 for (o, c) in out.iter_mut().zip(bytes.chunks_exact(2)) {
379 *o = half::f16::from_le_bytes([c[0], c[1]]).to_f32();
380 }
381 out
382 }
383 DType::BF16 => {
384 let mut out = vec![0f32; bytes.len() / 2];
385 #[cfg(target_endian = "little")]
386 {
387 let (head, mid, tail) = unsafe { bytes.align_to::<half::bf16>() };
388 if head.is_empty() && tail.is_empty() {
389 mid.convert_to_f32_slice(&mut out);
390 return out;
391 }
392 }
393 for (o, c) in out.iter_mut().zip(bytes.chunks_exact(2)) {
394 *o = half::bf16::from_le_bytes([c[0], c[1]]).to_f32();
395 }
396 out
397 }
398 _ => Vec::new(),
399 }
400}
401
402fn encode_into(dst: &mut [u8], vals: &[f32], dtype: DType) {
408 match dtype {
409 DType::F32 => {
410 for (c, &v) in dst.chunks_exact_mut(4).zip(vals) {
411 c.copy_from_slice(&v.to_le_bytes());
412 }
413 }
414 DType::F16 => {
415 #[cfg(target_endian = "little")]
416 {
417 let (head, mid, tail) = unsafe { dst.align_to_mut::<half::f16>() };
418 if head.is_empty() && tail.is_empty() {
419 mid.convert_from_f32_slice(vals);
420 return;
421 }
422 }
423 for (c, &v) in dst.chunks_exact_mut(2).zip(vals) {
424 c.copy_from_slice(&half::f16::from_f32(v).to_le_bytes());
425 }
426 }
427 DType::BF16 => {
428 #[cfg(target_endian = "little")]
429 {
430 let (head, mid, tail) = unsafe { dst.align_to_mut::<half::bf16>() };
431 if head.is_empty() && tail.is_empty() {
432 mid.convert_from_f32_slice(vals);
433 return;
434 }
435 }
436 for (c, &v) in dst.chunks_exact_mut(2).zip(vals) {
437 c.copy_from_slice(&half::bf16::from_f32(v).to_le_bytes());
438 }
439 }
440 _ => {}
441 }
442}
443
444fn reduce_into(dst: &mut [u8], incoming: &[u8], dtype: DType, op: ReduceKind) {
447 if is_reducible_int(dtype) {
448 let mut acc = decode_to_i32(dst, dtype);
449 let b = decode_to_i32(incoming, dtype);
450 for (x, &y) in acc.iter_mut().zip(&b) {
451 *x = combine_i32(op, *x, y);
452 }
453 encode_into_i32(dst, &acc, dtype);
454 } else if dtype == DType::F64 {
455 let a = decode_to_f64(dst, dtype);
456 let b = decode_to_f64(incoming, dtype);
457 let m: Vec<f64> = a
458 .iter()
459 .zip(&b)
460 .map(|(&x, &y)| combine64(op, x, y))
461 .collect();
462 dst.copy_from_slice(&encode_from_f64(&m, dtype));
463 } else {
464 let mut acc = decode_to_f32(dst, dtype);
465 let b = decode_to_f32(incoming, dtype);
466 for (x, &y) in acc.iter_mut().zip(&b) {
467 *x = combine32(op, *x, y);
468 }
469 encode_into(dst, &acc, dtype);
470 }
471}
472
473fn scale_mean(data: &mut [u8], dtype: DType, n: usize) {
476 if is_reducible_int(dtype) {
477 let half = n as i32 / 2;
479 let mut acc = decode_to_i32(data, dtype);
480 for v in acc.iter_mut() {
481 *v = (*v + if *v >= 0 { half } else { -half }) / n as i32;
482 }
483 encode_into_i32(data, &acc, dtype);
484 } else if dtype == DType::F64 {
485 let scaled: Vec<f64> = decode_to_f64(data, dtype)
486 .iter()
487 .map(|&v| v / n as f64)
488 .collect();
489 data.copy_from_slice(&encode_from_f64(&scaled, dtype));
490 } else {
491 let inv = 1.0 / n as f32;
492 let mut acc = decode_to_f32(data, dtype);
493 for v in acc.iter_mut() {
494 *v *= inv;
495 }
496 encode_into(data, &acc, dtype);
497 }
498}
499
500pub fn negotiate_reduce_dtype(dtypes: &[DType]) -> DType {
506 if dtypes.contains(&DType::F64) {
507 DType::F64
508 } else {
509 DType::F32
510 }
511}
512
513#[derive(Clone)]
520pub struct ProcessGroup {
521 transport: Arc<dyn Transport>,
522}
523
524impl ProcessGroup {
525 pub fn new(transport: Arc<dyn Transport>) -> Self {
526 Self { transport }
527 }
528
529 pub fn rank(&self) -> u32 {
530 self.transport.rank()
531 }
532
533 pub fn world_size(&self) -> u32 {
534 self.transport.world_size()
535 }
536
537 pub fn is_leader(&self) -> bool {
540 self.rank() == 0
541 }
542
543 pub fn transport(&self) -> &Arc<dyn Transport> {
544 &self.transport
545 }
546
547 pub fn barrier(&self) -> Result<(), CollectiveError> {
548 self.transport.barrier()
549 }
550
551 pub fn send_f32(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
556 debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
557 self.send_f32_tagged(to, tag, data)
558 }
559
560 pub fn recv_f32(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
562 debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
563 self.recv_f32_tagged(from, tag)
564 }
565
566 fn send_f32_tagged(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
569 self.transport.send_bytes(to, tag, &f32_to_le_bytes(data))
570 }
571
572 fn recv_f32_tagged(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
573 le_bytes_to_f32(&self.transport.recv_bytes(from, tag)?)
574 }
575
576 pub fn all_reduce(&self, data: &mut [f32], op: ReduceKind) -> Result<(), CollectiveError> {
588 self.all_reduce_mode(data, op, env_reduce_mode())
589 }
590
591 pub fn all_reduce_mode(
594 &self,
595 data: &mut [f32],
596 op: ReduceKind,
597 mode: ReduceMode,
598 ) -> Result<(), CollectiveError> {
599 let n = self.world_size();
600 if n <= 1 {
601 for v in data.iter_mut() {
602 *v = finalize(op, *v, n.max(1));
603 }
604 return Ok(());
605 }
606 match mode {
607 ReduceMode::Ring => self.all_reduce_ring_f32(data, op),
608 ReduceMode::Deterministic => self.all_reduce_deterministic(data, op),
609 }
610 }
611
612 fn all_reduce_ring_f32(&self, data: &mut [f32], op: ReduceKind) -> Result<(), CollectiveError> {
614 let n = self.world_size() as usize;
615 let me = self.rank() as usize;
616 let next = ((me + 1) % n) as u32;
617 let prev = ((me + n - 1) % n) as u32;
618 let len = data.len();
619
620 for step in 0..n - 1 {
624 let (ss, se) = ring_chunk(len, n, (me + n - step) % n);
625 let (rs, re) = ring_chunk(len, n, (me + n - step - 1) % n);
626 self.send_f32_tagged(next, TAG_ALL_REDUCE, &data[ss..se])?;
627 let incoming = self.recv_f32_tagged(prev, TAG_ALL_REDUCE)?;
628 expect_len(incoming.len(), re - rs)?;
629 for (d, v) in data[rs..re].iter_mut().zip(incoming) {
630 *d = combine(op, *d, v);
631 }
632 }
633 let (os, oe) = ring_chunk(len, n, (me + 1) % n);
637 for v in data[os..oe].iter_mut() {
638 *v = finalize(op, *v, n as u32);
639 }
640 self.ring_all_gather(data, n, me)
641 }
642
643 fn ring_all_gather(
647 &self,
648 data: &mut [f32],
649 n: usize,
650 me: usize,
651 ) -> Result<(), CollectiveError> {
652 let next = ((me + 1) % n) as u32;
653 let prev = ((me + n - 1) % n) as u32;
654 let len = data.len();
655 for step in 0..n - 1 {
656 let (ss, se) = ring_chunk(len, n, (me + n - step + 1) % n);
657 let (rs, re) = ring_chunk(len, n, (me + n - step) % n);
658 self.send_f32_tagged(next, TAG_ALL_REDUCE, &data[ss..se])?;
659 let incoming = self.recv_f32_tagged(prev, TAG_ALL_REDUCE)?;
660 expect_len(incoming.len(), re - rs)?;
661 data[rs..re].copy_from_slice(&incoming);
662 }
663 Ok(())
664 }
665
666 fn all_reduce_deterministic(
676 &self,
677 data: &mut [f32],
678 op: ReduceKind,
679 ) -> Result<(), CollectiveError> {
680 let n = self.world_size() as usize;
681 let me = self.rank() as usize;
682 let next = ((me + 1) % n) as u32;
683 let prev = ((me + n - 1) % n) as u32;
684 let len = data.len();
685
686 let mut acc: Vec<f64> = data.iter().map(|&v| v as f64).collect();
688 let mut send = Vec::with_capacity((len / n + 1) * 8);
690
691 for step in 0..n - 1 {
694 let (ss, se) = ring_chunk(len, n, (me + n - step) % n);
695 let (rs, re) = ring_chunk(len, n, (me + n - step - 1) % n);
696 send.clear();
697 send.extend(acc[ss..se].iter().flat_map(|v| v.to_le_bytes()));
698 self.transport.send_bytes(next, TAG_ALL_REDUCE, &send)?;
699 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
700 expect_len(incoming.len(), (re - rs) * 8)?;
701 for (a, c) in acc[rs..re].iter_mut().zip(incoming.chunks_exact(8)) {
702 *a = combine64(op, *a, f64::from_le_bytes(c.try_into().unwrap()));
703 }
704 }
705
706 let (os, oe) = ring_chunk(len, n, (me + 1) % n);
709 for i in os..oe {
710 data[i] = finalize64(op, acc[i], n as u32) as f32;
711 }
712 self.ring_all_gather(data, n, me)
713 }
714
715 pub fn all_reduce_typed(
723 &self,
724 data: &mut [u8],
725 dtype: DType,
726 op: ReduceKind,
727 ) -> Result<(), CollectiveError> {
728 if !is_reducible_float(dtype) && !is_reducible_int(dtype) {
729 return Err(CollectiveError::TransportError {
730 reason: format!("all_reduce_typed: unsupported dtype {dtype:?}"),
731 });
732 }
733 let esz = dtype.size_bytes();
734 if esz == 0 || !data.len().is_multiple_of(esz) {
735 return Err(CollectiveError::TransportError {
736 reason: format!(
737 "all_reduce_typed: {} bytes not a multiple of {esz}",
738 data.len()
739 ),
740 });
741 }
742 let n = self.world_size();
743 if n <= 1 {
744 return Ok(()); }
746 let n = n as usize;
747 let me = self.rank() as usize;
748 let next = ((me + 1) % n) as u32;
749 let prev = ((me + n - 1) % n) as u32;
750
751 let elems = data.len() / esz;
752 let base = elems / n;
753 let rem = elems % n;
754 let ebound = |i: usize| (i * base + i.min(rem)) * esz; let chunk = |i: usize| (ebound(i), ebound(i + 1));
756
757 for step in 0..n - 1 {
759 let (ss, se) = chunk((me + n - step) % n);
760 let (rs, re) = chunk((me + n - step - 1) % n);
761 self.transport
762 .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
763 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
764 if incoming.len() != re - rs {
765 return Err(CollectiveError::LengthMismatch {
766 expected: re - rs,
767 got: incoming.len(),
768 });
769 }
770 reduce_into(&mut data[rs..re], &incoming, dtype, op);
771 }
772 for step in 0..n - 1 {
774 let (ss, se) = chunk((me + n - step + 1) % n);
775 let (rs, re) = chunk((me + n - step) % n);
776 self.transport
777 .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
778 let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
779 if incoming.len() != re - rs {
780 return Err(CollectiveError::LengthMismatch {
781 expected: re - rs,
782 got: incoming.len(),
783 });
784 }
785 data[rs..re].copy_from_slice(&incoming);
786 }
787 if matches!(op, ReduceKind::Mean) {
788 scale_mean(data, dtype, n);
789 }
790 Ok(())
791 }
792
793 pub fn spawn_all_reduce(
799 self: &Arc<Self>,
800 mut data: Vec<f32>,
801 op: ReduceKind,
802 ) -> std::thread::JoinHandle<Vec<f32>> {
803 let g = self.clone();
804 std::thread::spawn(move || {
805 g.all_reduce(&mut data, op).expect("background all_reduce");
806 data
807 })
808 }
809
810 pub fn all_gather(&self, local: &[f32]) -> Result<Vec<f32>, CollectiveError> {
814 let n = self.world_size();
815 let len = local.len();
816 if n <= 1 {
817 return Ok(local.to_vec());
818 }
819 if self.rank() == 0 {
820 let mut out = vec![0f32; n as usize * len];
821 out[..len].copy_from_slice(local);
822 for r in 1..n {
823 let chunk = self.recv_f32_tagged(r, TAG_ALL_GATHER)?;
824 if chunk.len() != len {
825 return Err(CollectiveError::LengthMismatch {
826 expected: len,
827 got: chunk.len(),
828 });
829 }
830 let start = r as usize * len;
831 out[start..start + len].copy_from_slice(&chunk);
832 }
833 for r in 1..n {
834 self.send_f32_tagged(r, TAG_ALL_GATHER, &out)?;
835 }
836 Ok(out)
837 } else {
838 self.send_f32_tagged(0, TAG_ALL_GATHER, local)?;
839 let out = self.recv_f32_tagged(0, TAG_ALL_GATHER)?;
840 if out.len() != n as usize * len {
841 return Err(CollectiveError::LengthMismatch {
842 expected: n as usize * len,
843 got: out.len(),
844 });
845 }
846 Ok(out)
847 }
848 }
849
850 pub fn broadcast(&self, root: u32, data: &mut [f32]) -> Result<(), CollectiveError> {
853 let n = self.world_size();
854 if n <= 1 {
855 return Ok(());
856 }
857 if self.rank() == root {
858 for r in 0..n {
859 if r != root {
860 self.send_f32_tagged(r, TAG_BROADCAST, data)?;
861 }
862 }
863 } else {
864 let res = self.recv_f32_tagged(root, TAG_BROADCAST)?;
865 if res.len() != data.len() {
866 return Err(CollectiveError::LengthMismatch {
867 expected: data.len(),
868 got: res.len(),
869 });
870 }
871 data.copy_from_slice(&res);
872 }
873 Ok(())
874 }
875
876 pub fn all_to_all(&self, data: &[f32]) -> Result<Vec<f32>, CollectiveError> {
886 let n = self.world_size() as usize;
887 if n <= 1 {
888 return Ok(data.to_vec());
889 }
890 if !data.len().is_multiple_of(n) {
891 return Err(CollectiveError::LengthMismatch {
892 expected: n,
893 got: data.len(),
894 });
895 }
896 let chunk = data.len() / n;
897 let rank = self.rank() as usize;
898 let mut out = vec![0f32; data.len()];
899 out[rank * chunk..(rank + 1) * chunk]
900 .copy_from_slice(&data[rank * chunk..(rank + 1) * chunk]);
901 for p in 0..n {
902 if p == rank {
903 continue;
904 }
905 let send = &data[p * chunk..(p + 1) * chunk];
906 if rank < p {
908 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL, send)?;
909 let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL)?;
910 out[p * chunk..(p + 1) * chunk].copy_from_slice(&r);
911 } else {
912 let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL)?;
913 out[p * chunk..(p + 1) * chunk].copy_from_slice(&r);
914 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL, send)?;
915 }
916 }
917 Ok(out)
918 }
919
920 pub fn all_to_all_v(
931 &self,
932 data: &[f32],
933 send_counts: &[usize],
934 ) -> Result<(Vec<f32>, Vec<usize>), CollectiveError> {
935 let n = self.world_size() as usize;
936 if send_counts.len() != n {
937 return Err(CollectiveError::LengthMismatch {
938 expected: n,
939 got: send_counts.len(),
940 });
941 }
942 let total_send: usize = send_counts.iter().sum();
943 if data.len() != total_send {
944 return Err(CollectiveError::LengthMismatch {
945 expected: total_send,
946 got: data.len(),
947 });
948 }
949 if n <= 1 {
950 return Ok((data.to_vec(), send_counts.to_vec()));
951 }
952
953 let rank = self.rank() as usize;
954 let mut send_off = vec![0usize; n + 1];
955 for i in 0..n {
956 send_off[i + 1] = send_off[i] + send_counts[i];
957 }
958
959 let mut recv_counts = vec![0usize; n];
961 recv_counts[rank] = send_counts[rank];
962 for p in 0..n {
963 if p == rank {
964 continue;
965 }
966 let my_count = (send_counts[p] as u64).to_le_bytes();
967 let their = if rank < p {
968 self.transport
969 .send_bytes(p as u32, TAG_ALL_TO_ALL_V, &my_count)?;
970 self.transport.recv_bytes(p as u32, TAG_ALL_TO_ALL_V)?
971 } else {
972 let r = self.transport.recv_bytes(p as u32, TAG_ALL_TO_ALL_V)?;
973 self.transport
974 .send_bytes(p as u32, TAG_ALL_TO_ALL_V, &my_count)?;
975 r
976 };
977 if their.len() != 8 {
978 return Err(CollectiveError::TransportError {
979 reason: format!(
980 "all_to_all_v: count from rank {p} has {} bytes, want 8",
981 their.len()
982 ),
983 });
984 }
985 recv_counts[p] = u64::from_le_bytes(their[..].try_into().unwrap()) as usize;
986 }
987
988 let mut recv_off = vec![0usize; n + 1];
989 for i in 0..n {
990 recv_off[i + 1] = recv_off[i] + recv_counts[i];
991 }
992 let mut out = vec![0f32; recv_off[n]];
993 out[recv_off[rank]..recv_off[rank + 1]]
994 .copy_from_slice(&data[send_off[rank]..send_off[rank + 1]]);
995
996 for p in 0..n {
998 if p == rank {
999 continue;
1000 }
1001 let send = &data[send_off[p]..send_off[p + 1]];
1002 let got = if rank < p {
1003 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL_V, send)?;
1004 self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL_V)?
1005 } else {
1006 let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL_V)?;
1007 self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL_V, send)?;
1008 r
1009 };
1010 if got.len() != recv_counts[p] {
1011 return Err(CollectiveError::LengthMismatch {
1012 expected: recv_counts[p],
1013 got: got.len(),
1014 });
1015 }
1016 out[recv_off[p]..recv_off[p + 1]].copy_from_slice(&got);
1017 }
1018 Ok((out, recv_counts))
1019 }
1020
1021 pub fn reduce(
1025 &self,
1026 root: u32,
1027 data: &mut [f32],
1028 op: ReduceKind,
1029 ) -> Result<(), CollectiveError> {
1030 let n = self.world_size();
1031 if n <= 1 {
1032 return Ok(());
1033 }
1034 if self.rank() == root {
1035 let mut acc = data.to_vec();
1036 for r in 0..n {
1037 if r == root {
1038 continue;
1039 }
1040 let other = self.recv_f32_tagged(r, TAG_REDUCE)?;
1041 if other.len() != acc.len() {
1042 return Err(CollectiveError::LengthMismatch {
1043 expected: acc.len(),
1044 got: other.len(),
1045 });
1046 }
1047 for i in 0..acc.len() {
1048 acc[i] = op.fold(acc[i], other[i]);
1049 }
1050 }
1051 for v in acc.iter_mut() {
1052 *v = op.finalize(*v, n as usize);
1053 }
1054 data.copy_from_slice(&acc);
1055 } else {
1056 self.send_f32_tagged(root, TAG_REDUCE, data)?;
1057 }
1058 Ok(())
1059 }
1060
1061 pub fn ppermute(&self, data: &[f32], perm: &[(u32, u32)]) -> Result<Vec<f32>, CollectiveError> {
1065 let rank = self.rank();
1066 let my_dst = perm.iter().find(|&&(s, _)| s == rank).map(|&(_, d)| d);
1067 let my_src = perm.iter().find(|&&(_, d)| d == rank).map(|&(s, _)| s);
1068 if let Some(dst) = my_dst {
1070 if dst != rank {
1071 self.send_f32_tagged(dst, TAG_PPERMUTE, data)?;
1072 }
1073 }
1074 let out = match my_src {
1076 Some(src) if src == rank => data.to_vec(),
1077 Some(src) => self.recv_f32_tagged(src, TAG_PPERMUTE)?,
1078 None => vec![0f32; data.len()],
1079 };
1080 Ok(out)
1081 }
1082
1083 pub fn federated_average(
1090 &self,
1091 data: &mut [f32],
1092 deadline: Duration,
1093 ) -> Result<usize, CollectiveError> {
1094 let n = self.world_size();
1095 if n <= 1 {
1096 return Ok(1);
1097 }
1098 let elems = data.len();
1099 if self.rank() == 0 {
1100 let mut acc = data.to_vec();
1101 let mut present = 1usize;
1102 let end = Instant::now() + deadline;
1103 for r in 1..n {
1104 let remaining = end.saturating_duration_since(Instant::now());
1105 if let Some(bytes) =
1106 self.transport
1107 .recv_bytes_timeout(r, TAG_ALL_REDUCE, remaining)?
1108 {
1109 let other = le_bytes_to_f32(&bytes)?;
1110 if other.len() == elems {
1111 for i in 0..elems {
1112 acc[i] += other[i];
1113 }
1114 present += 1;
1115 }
1116 }
1117 }
1118 let inv = 1.0 / present as f32;
1119 for v in acc.iter_mut() {
1120 *v *= inv;
1121 }
1122 data.copy_from_slice(&acc);
1123 for r in 1..n {
1124 self.send_f32_tagged(r, TAG_ALL_REDUCE, &acc)?;
1125 }
1126 Ok(present)
1127 } else {
1128 self.send_f32_tagged(0, TAG_ALL_REDUCE, data)?;
1129 let res = self.recv_f32_tagged(0, TAG_ALL_REDUCE)?;
1130 if res.len() == elems {
1131 data.copy_from_slice(&res);
1132 }
1133 Ok(0)
1134 }
1135 }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140 use super::*;
1141 use std::collections::{HashMap, VecDeque};
1142 use std::sync::{Condvar, Mutex};
1143
1144 struct ChannelTransport {
1148 rank: u32,
1149 world: u32,
1150 mailbox: Arc<(Mutex<HashMap<(u32, u32, u32), VecDeque<Vec<u8>>>>, Condvar)>,
1151 }
1152
1153 impl ChannelTransport {
1154 fn fan_out(world: u32) -> Vec<Arc<ChannelTransport>> {
1155 let mailbox = Arc::new((Mutex::new(HashMap::new()), Condvar::new()));
1156 (0..world)
1157 .map(|r| {
1158 Arc::new(ChannelTransport {
1159 rank: r,
1160 world,
1161 mailbox: mailbox.clone(),
1162 })
1163 })
1164 .collect()
1165 }
1166 }
1167
1168 impl Transport for ChannelTransport {
1169 fn rank(&self) -> u32 {
1170 self.rank
1171 }
1172 fn world_size(&self) -> u32 {
1173 self.world
1174 }
1175 fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError> {
1176 let (m, cv) = &*self.mailbox;
1177 m.lock()
1178 .unwrap()
1179 .entry((to, self.rank, tag))
1180 .or_default()
1181 .push_back(bytes.to_vec());
1182 cv.notify_all();
1183 Ok(())
1184 }
1185 fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError> {
1186 let (m, cv) = &*self.mailbox;
1187 let mut guard = m.lock().unwrap();
1188 loop {
1189 if let Some(q) = guard.get_mut(&(self.rank, from, tag))
1190 && let Some(v) = q.pop_front()
1191 {
1192 return Ok(v);
1193 }
1194 guard = cv.wait(guard).unwrap();
1195 }
1196 }
1197 }
1198
1199 fn run_ranks<F>(world: u32, body: F) -> Vec<()>
1200 where
1201 F: Fn(ProcessGroup) + Send + Sync + 'static,
1202 {
1203 let ts = ChannelTransport::fan_out(world);
1204 let body = Arc::new(body);
1205 let handles: Vec<_> = ts
1206 .into_iter()
1207 .map(|t| {
1208 let body = body.clone();
1209 std::thread::spawn(move || body(ProcessGroup::new(t)))
1210 })
1211 .collect();
1212 handles.into_iter().map(|h| h.join().unwrap()).collect()
1213 }
1214
1215 #[test]
1216 fn all_reduce_sum_matches_serial() {
1217 run_ranks(4, |g| {
1218 let r = g.rank() as f32;
1219 let mut data = vec![r + 1.0; 3]; g.all_reduce(&mut data, ReduceKind::Sum).unwrap();
1221 assert_eq!(data, vec![10.0; 3], "rank {}", g.rank());
1222 });
1223 }
1224
1225 #[test]
1226 fn all_reduce_modes_agree_on_the_sum() {
1227 for mode in [ReduceMode::Ring, ReduceMode::Deterministic] {
1229 run_ranks(4, move |g| {
1230 let r = g.rank() as f32 + 1.0; let mut data = vec![r, r * 2.0, r * 3.0];
1232 g.all_reduce_mode(&mut data, ReduceKind::Sum, mode).unwrap();
1233 assert_eq!(
1234 data,
1235 vec![10.0, 20.0, 30.0],
1236 "mode {mode:?} rank {}",
1237 g.rank()
1238 );
1239 });
1240 }
1241 }
1242
1243 #[test]
1244 fn deterministic_reduce_mean_divides() {
1245 run_ranks(4, |g| {
1246 let mut data = vec![g.rank() as f32 + 1.0; 2]; g.all_reduce_mode(&mut data, ReduceKind::Mean, ReduceMode::Deterministic)
1248 .unwrap();
1249 assert_eq!(data, vec![2.5, 2.5], "rank {}", g.rank());
1250 });
1251 }
1252
1253 #[test]
1254 fn deterministic_is_exact_where_the_f32_ring_would_lose_precision() {
1255 run_ranks(4, |g| {
1260 let v = match g.rank() {
1261 0 => 1e8f32,
1262 1 => -1e8f32,
1263 _ => 4.0f32,
1264 };
1265 let mut data = vec![v, v]; g.all_reduce_mode(&mut data, ReduceKind::Sum, ReduceMode::Deterministic)
1267 .unwrap();
1268 assert_eq!(data, vec![8.0, 8.0], "rank {}", g.rank());
1269 });
1270 }
1271
1272 #[test]
1273 fn deterministic_is_world_size_independent_for_replicated_input() {
1274 for &n in &[2u32, 3, 5, 7] {
1278 run_ranks(n, |g| {
1279 let mut data = vec![0.1f32, 0.2, 0.3, 0.7];
1280 g.all_reduce_mode(&mut data, ReduceKind::Mean, ReduceMode::Deterministic)
1281 .unwrap();
1282 assert_eq!(
1283 data,
1284 vec![0.1, 0.2, 0.3, 0.7],
1285 "n={} rank {}",
1286 g.world_size(),
1287 g.rank()
1288 );
1289 });
1290 }
1291 }
1292
1293 #[test]
1294 fn all_gather_concatenates_in_rank_order() {
1295 run_ranks(3, |g| {
1296 let r = g.rank() as f32;
1297 let out = g.all_gather(&[10.0 * r, 10.0 * r + 1.0]).unwrap();
1298 assert_eq!(
1299 out,
1300 vec![0.0, 1.0, 10.0, 11.0, 20.0, 21.0],
1301 "rank {}",
1302 g.rank()
1303 );
1304 });
1305 }
1306
1307 #[test]
1308 fn broadcast_from_root_overwrites() {
1309 run_ranks(4, |g| {
1310 let mut data = if g.is_leader() {
1311 vec![7.0, 8.0, 9.0]
1312 } else {
1313 vec![0.0, 0.0, 0.0]
1314 };
1315 g.broadcast(0, &mut data).unwrap();
1316 assert_eq!(data, vec![7.0, 8.0, 9.0], "rank {}", g.rank());
1317 });
1318 }
1319
1320 #[test]
1321 fn all_to_all_v_variable_chunks() {
1322 run_ranks(3, |g| {
1325 let n = g.world_size() as usize;
1326 let r = g.rank() as usize;
1327 let send_counts: Vec<usize> = (0..n).map(|j| j + 1).collect();
1328 let mut data = Vec::new();
1329 for (j, &c) in send_counts.iter().enumerate() {
1330 for _ in 0..c {
1331 data.push((100 * r + j) as f32);
1332 }
1333 }
1334 let (recv, recv_counts) = g.all_to_all_v(&data, &send_counts).unwrap();
1335 assert_eq!(recv_counts, vec![r + 1; n], "rank {r} recv_counts");
1336 let mut off = 0usize;
1337 for (j, &c) in recv_counts.iter().enumerate() {
1338 let chunk = &recv[off..off + c];
1339 assert!(
1340 chunk
1341 .iter()
1342 .all(|&v| (v - (100 * j + r) as f32).abs() < 1e-6),
1343 "rank {r} from {j}: {chunk:?}"
1344 );
1345 off += c;
1346 }
1347 });
1348 }
1349
1350 #[test]
1351 fn barrier_round_trips() {
1352 run_ranks(4, |g| {
1353 g.barrier().unwrap();
1354 g.barrier().unwrap(); });
1356 }
1357
1358 #[test]
1359 fn point_to_point_ring_handoff() {
1360 run_ranks(3, |g| {
1363 let n = g.world_size();
1364 let me = g.rank();
1365 let next = (me + 1) % n;
1366 let prev = (me + n - 1) % n;
1367 if me % 2 == 0 {
1370 g.send_f32(next, 1, &[me as f32]).unwrap();
1371 let got = g.recv_f32(prev, 1).unwrap();
1372 assert_eq!(got, vec![prev as f32]);
1373 } else {
1374 let got = g.recv_f32(prev, 1).unwrap();
1375 assert_eq!(got, vec![prev as f32]);
1376 g.send_f32(next, 1, &[me as f32]).unwrap();
1377 }
1378 });
1379 }
1380
1381 fn f16_bytes(vals: &[f32]) -> Vec<u8> {
1382 vals.iter()
1383 .flat_map(|&v| half::f16::from_f32(v).to_le_bytes())
1384 .collect()
1385 }
1386 fn f16_vals(bytes: &[u8]) -> Vec<f32> {
1387 bytes
1388 .chunks_exact(2)
1389 .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f32())
1390 .collect()
1391 }
1392
1393 #[test]
1394 fn all_reduce_typed_f16_sums_across_ranks() {
1395 run_ranks(4, |g| {
1396 let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 3]); g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1398 .unwrap();
1399 assert_eq!(f16_vals(&bytes), vec![10.0; 3], "rank {}", g.rank());
1400 });
1401 }
1402
1403 #[test]
1404 fn all_reduce_typed_bf16_mean() {
1405 run_ranks(4, |g| {
1406 let mut bytes: Vec<u8> = [g.rank() as f32 + 1.0; 4]
1407 .iter()
1408 .flat_map(|&v| half::bf16::from_f32(v).to_le_bytes())
1409 .collect();
1410 g.all_reduce_typed(&mut bytes, DType::BF16, ReduceKind::Mean)
1411 .unwrap();
1412 let got: Vec<f32> = bytes
1413 .chunks_exact(2)
1414 .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f32())
1415 .collect();
1416 assert_eq!(got, vec![2.5; 4], "rank {}", g.rank());
1418 });
1419 }
1420
1421 #[test]
1422 fn all_reduce_typed_uneven_length() {
1423 run_ranks(4, |g| {
1425 let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 5]);
1426 g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1427 .unwrap();
1428 assert_eq!(f16_vals(&bytes), vec![10.0; 5], "rank {}", g.rank());
1429 });
1430 }
1431
1432 #[test]
1433 fn all_reduce_typed_f16_large_buffer_simd_path() {
1434 run_ranks(3, |g| {
1438 let mut bytes = f16_bytes(&vec![g.rank() as f32 + 1.0; 1000]); g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1440 .unwrap();
1441 assert_eq!(f16_vals(&bytes), vec![6.0; 1000], "rank {}", g.rank());
1442 });
1443 }
1444
1445 #[test]
1446 fn all_reduce_typed_i8_federated_mean() {
1447 run_ranks(3, |g| {
1449 let mut bytes = vec![((g.rank() + 1) * 10) as i8 as u8; 4]; g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Mean)
1451 .unwrap();
1452 let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1453 assert_eq!(got, vec![20i8; 4], "rank {}", g.rank()); });
1455 }
1456
1457 #[test]
1458 fn all_reduce_typed_i8_sum_saturates() {
1459 run_ranks(2, |g| {
1460 let mut bytes = vec![100i8 as u8; 2];
1461 g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Sum)
1462 .unwrap();
1463 let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1464 assert_eq!(got, vec![127i8; 2], "rank {}", g.rank()); });
1466 }
1467
1468 #[test]
1469 fn negotiate_reduce_dtype_rules() {
1470 assert_eq!(
1471 negotiate_reduce_dtype(&[DType::F16, DType::BF16]),
1472 DType::F32
1473 );
1474 assert_eq!(
1475 negotiate_reduce_dtype(&[DType::F16, DType::F64]),
1476 DType::F64
1477 );
1478 assert_eq!(negotiate_reduce_dtype(&[]), DType::F32);
1479 }
1480}