Skip to main content

rlx_driver/
transport.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Two-sided point-to-point transport + process group (plan #12/#49,
6//! multi-node extension).
7//!
8//! [`SymmetricTransport`](crate::symmetric::SymmetricTransport) models
9//! *one-sided* `put`/`get` — the right shape for RDMA and for the
10//! tensor-parallel all-reduce that lives *inside* a compiled graph.
11//! Pipeline parallelism wants the complementary *two-sided* surface:
12//! rank `r` hands its hidden state to rank `r-1` with a matched
13//! `send`/`recv`. This module adds that surface ([`Transport`]) plus a
14//! [`ProcessGroup`] that layers tensor-shaped collectives
15//! (`all_reduce` / `all_gather` / `broadcast`) on top.
16//!
17//! The collectives here are the *gather-to-root* family: simple,
18//! deadlock-free over any correct [`Transport`], and good enough for
19//! the small rank counts (≤ 8) a few Thunderbolt-linked machines hit.
20//! Bandwidth-optimal ring variants are a later optimization — the
21//! `ProcessGroup` API does not change when they land.
22//!
23//! Three transports implement [`Transport`]:
24//!   - [`TcpTransport`](crate::net::TcpTransport) — portable TCP, works
25//!     over Ethernet and the Thunderbolt Bridge IP link.
26//!   - [`ThunderboltTransport`](crate::net::ThunderboltTransport) —
27//!     TCP pinned to the Thunderbolt interface, also exposing the
28//!     symmetric one-sided heap.
29//!   - `MlxTransport` (in `rlx-mlx`) — delegates to MLX's `jaccl`
30//!     distributed backend over FFI.
31
32use 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
39/// Tags `>= TAG_RESERVED_BASE` are reserved for the collective and
40/// barrier machinery. User point-to-point traffic (pipeline-parallel
41/// hidden states) must use tags below this.
42pub 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;
50/// Variable-size all-to-all (counts then payloads); distinct from equal-chunk
51/// [`TAG_ALL_TO_ALL`] so the two can coexist in one graph without tag clashes.
52const TAG_ALL_TO_ALL_V: u32 = TAG_RESERVED_BASE + 7;
53
54/// Two-sided, tagged, byte-oriented point-to-point transport between
55/// `world_size` ranks.
56///
57/// Implementations must be safe to share across threads (`Send +
58/// Sync`) and must let a reader on one rank pull a message that a
59/// writer on another rank pushed with the same `(src, tag)` — the
60/// `send`/`recv` are *matched*, like MPI. `recv` learns the payload
61/// length from the wire, so callers need not know it in advance.
62pub trait Transport: Send + Sync {
63    /// This process's rank in `0..world_size`.
64    fn rank(&self) -> u32;
65    /// Total number of participating ranks.
66    fn world_size(&self) -> u32;
67
68    /// Send `bytes` to rank `to`, tagged `tag`. Returns once the bytes
69    /// are handed to the transport (not necessarily delivered).
70    fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError>;
71
72    /// Block until a message with matching `(from, tag)` arrives, and
73    /// return its payload.
74    fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError>;
75
76    /// Like [`Self::recv_bytes`] but gives up after `timeout`, returning
77    /// `Ok(None)`. The default blocks (transports with no deadline support
78    /// ignore the timeout); [`crate::NetTransport`] overrides it with a real
79    /// `wait_timeout`. This is the basis for not letting a slow or offline
80    /// edge node stall a collective (bounded staleness).
81    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    /// Block until every rank has reached this barrier.
91    ///
92    /// Default is a gather-to-root rendezvous over `send`/`recv`: every
93    /// non-root sends a token to rank 0 and waits for the release; rank
94    /// 0 collects all tokens then releases everyone. Lockstep is
95    /// preserved across repeated barriers because a non-root cannot
96    /// enter the next barrier until it observes the current release.
97    fn barrier(&self) -> Result<(), CollectiveError> {
98        default_barrier(self)
99    }
100}
101
102/// Gather-to-root barrier usable by any [`Transport`]. Pulled out as a
103/// free function so trait impls can reuse it from an overridden
104/// `barrier` if they want to wrap it.
105pub 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
125/// Little-endian flatten of an `f32` slice. Apple Silicon and x86 are
126/// both little-endian; we fix LE on the wire so a future big-endian
127/// rank would still interoperate.
128fn 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
136/// Inverse of [`f32_to_le_bytes`]. Errors if `bytes.len()` is not a
137/// multiple of 4.
138fn 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        .as_chunks::<4>()
146        .0
147        .iter()
148        .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
149        .collect())
150}
151
152fn combine(op: ReduceKind, a: f32, b: f32) -> f32 {
153    match op {
154        ReduceKind::Sum | ReduceKind::Mean => a + b,
155        ReduceKind::Max => a.max(b),
156        ReduceKind::Min => a.min(b),
157    }
158}
159
160fn finalize(op: ReduceKind, acc: f32, n: u32) -> f32 {
161    match op {
162        ReduceKind::Mean => acc / (n as f32),
163        _ => acc,
164    }
165}
166
167/// f64 mean-finalize for the deterministic reducer (divide once, in f64).
168fn finalize64(op: ReduceKind, acc: f64, n: u32) -> f64 {
169    match op {
170        ReduceKind::Mean => acc / (n as f64),
171        _ => acc,
172    }
173}
174
175/// `[start, end)` of chunk `i` in a near-equal `n`-way split of `len` — the
176/// first `len % n` chunks get one extra element. The single source of truth for
177/// every ring collective's chunk boundaries (both [`ProcessGroup::all_reduce`]
178/// rings partition the vector this way, and rank `me` ends up owning chunk
179/// `(me + 1) % n`).
180fn ring_chunk(len: usize, n: usize, i: usize) -> (usize, usize) {
181    let base = len / n;
182    let rem = len % n;
183    let bound = |j: usize| j * base + j.min(rem);
184    (bound(i), bound(i + 1))
185}
186
187/// Guard that a received frame is the length the ring expects for this step.
188fn expect_len(got: usize, want: usize) -> Result<(), CollectiveError> {
189    if got != want {
190        return Err(CollectiveError::LengthMismatch {
191            expected: want,
192            got,
193        });
194    }
195    Ok(())
196}
197
198/// Reduction algorithm + precision for [`ProcessGroup::all_reduce`]. Selected
199/// per call, or globally via env (see [`env_reduce_mode`]).
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum ReduceMode {
202    /// Bandwidth-optimal ring, **f32** accumulation. Fastest; f32 precision, and
203    /// the last-ulp result depends on world size. Reproducible run-to-run. Default.
204    Ring,
205    /// **Deterministic + precise, at ring speed.** The same bandwidth-optimal ring,
206    /// but its reduce-scatter **accumulates in f64** (the all-gather stays f32 —
207    /// those values are already reduced, so there is no precision left to lose).
208    /// The fold order is fixed by the algorithm, so the result is bitwise
209    /// reproducible run-to-run; f64 accumulation makes each element the
210    /// correctly-rounded exact cross-rank sum, so it neither loses precision nor
211    /// biases the gradient and is effectively independent of world size. Only the
212    /// reduce-scatter carries an f64 payload (≈1.5× the f32 ring's bytes) — NOT a
213    /// gather-to-root's `O(n·len)`, so speed is preserved. The mode to pick when
214    /// you want reproducible, precise data-parallel training.
215    Deterministic,
216}
217
218/// Global reduction mode from the environment (read once), so a whole cluster
219/// opts into deterministic + precise reductions with no code change:
220///   `RLX_DETERMINISTIC_REDUCE=1` → [`ReduceMode::Deterministic`]
221///   (unset)                      → [`ReduceMode::Ring`]
222/// **Every rank must agree** (mismatched modes mix incompatible message sizes and
223/// would desync), so set it on all nodes — the same discipline as any other
224/// cluster-wide `RLX_*` flag.
225pub fn env_reduce_mode() -> ReduceMode {
226    static MODE: std::sync::OnceLock<ReduceMode> = std::sync::OnceLock::new();
227    *MODE.get_or_init(|| {
228        let on = |k: &str| {
229            std::env::var(k)
230                .map(|v| v != "0" && !v.is_empty())
231                .unwrap_or(false)
232        };
233        if on("RLX_DETERMINISTIC_REDUCE") {
234            ReduceMode::Deterministic
235        } else {
236            ReduceMode::Ring
237        }
238    })
239}
240
241/// f64 reduction step — collectives accumulate in f64 regardless of the
242/// payload dtype, so f16/bf16 reductions don't lose range mid-sum.
243fn combine64(op: ReduceKind, a: f64, b: f64) -> f64 {
244    match op {
245        ReduceKind::Sum | ReduceKind::Mean => a + b,
246        ReduceKind::Max => a.max(b),
247        ReduceKind::Min => a.min(b),
248    }
249}
250
251/// The float dtypes a collective can reduce over the wire.
252fn is_reducible_float(d: DType) -> bool {
253    matches!(d, DType::F16 | DType::BF16 | DType::F32 | DType::F64)
254}
255
256/// The integer dtypes a collective can reduce — the edge/quantized tier
257/// (MCU/FPGA/ANE). Accumulated in i32 so a sum of 8-bit lanes can't overflow.
258fn is_reducible_int(d: DType) -> bool {
259    matches!(d, DType::I8 | DType::U8)
260}
261
262fn decode_to_i32(bytes: &[u8], dtype: DType) -> Vec<i32> {
263    match dtype {
264        DType::I8 => bytes.iter().map(|&b| b as i8 as i32).collect(),
265        DType::U8 => bytes.iter().map(|&b| b as i32).collect(),
266        _ => Vec::new(),
267    }
268}
269
270fn encode_into_i32(dst: &mut [u8], vals: &[i32], dtype: DType) {
271    match dtype {
272        DType::I8 => {
273            for (d, &v) in dst.iter_mut().zip(vals) {
274                *d = v.clamp(i8::MIN as i32, i8::MAX as i32) as i8 as u8;
275            }
276        }
277        DType::U8 => {
278            for (d, &v) in dst.iter_mut().zip(vals) {
279                *d = v.clamp(0, u8::MAX as i32) as u8;
280            }
281        }
282        _ => {}
283    }
284}
285
286fn combine_i32(op: ReduceKind, a: i32, b: i32) -> i32 {
287    match op {
288        ReduceKind::Sum | ReduceKind::Mean => a + b,
289        ReduceKind::Max => a.max(b),
290        ReduceKind::Min => a.min(b),
291    }
292}
293
294/// Decode `bytes` of `dtype` to f64 for reduction. Little-endian on the wire.
295fn decode_to_f64(bytes: &[u8], dtype: DType) -> Vec<f64> {
296    match dtype {
297        DType::F32 => bytes
298            .as_chunks::<4>()
299            .0
300            .iter()
301            .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]) as f64)
302            .collect(),
303        DType::F64 => bytes
304            .as_chunks::<8>()
305            .0
306            .iter()
307            .map(|c| f64::from_le_bytes(*c))
308            .collect(),
309        DType::F16 => bytes
310            .as_chunks::<2>()
311            .0
312            .iter()
313            .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f64())
314            .collect(),
315        DType::BF16 => bytes
316            .as_chunks::<2>()
317            .0
318            .iter()
319            .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f64())
320            .collect(),
321        _ => Vec::new(),
322    }
323}
324
325/// Encode f64 reduction results back to `dtype` bytes (little-endian).
326fn encode_from_f64(vals: &[f64], dtype: DType) -> Vec<u8> {
327    let mut out = Vec::with_capacity(vals.len() * dtype.size_bytes().max(1));
328    match dtype {
329        DType::F32 => vals
330            .iter()
331            .for_each(|&v| out.extend_from_slice(&(v as f32).to_le_bytes())),
332        DType::F64 => vals
333            .iter()
334            .for_each(|&v| out.extend_from_slice(&v.to_le_bytes())),
335        DType::F16 => vals
336            .iter()
337            .for_each(|&v| out.extend_from_slice(&half::f16::from_f64(v).to_le_bytes())),
338        DType::BF16 => vals
339            .iter()
340            .for_each(|&v| out.extend_from_slice(&half::bf16::from_f64(v).to_le_bytes())),
341        _ => {}
342    }
343    out
344}
345
346/// f32 reduction step — the accumulator for f16/bf16/f32. f32 is exact for
347/// f16/bf16 inputs and matches rlx's native f32 collective bit-for-bit.
348fn combine32(op: ReduceKind, a: f32, b: f32) -> f32 {
349    match op {
350        ReduceKind::Sum | ReduceKind::Mean => a + b,
351        ReduceKind::Max => a.max(b),
352        ReduceKind::Min => a.min(b),
353    }
354}
355
356/// SIMD-batch decode of `dtype` bytes to f32. On little-endian targets (Apple
357/// Silicon + x86) f16/bf16 reinterpret in place and `half`'s vectorized
358/// `convert_to_f32_slice` runs ~an order of magnitude faster than the
359/// per-element scalar path; a scalar `from_le_bytes` fallback keeps big-endian
360/// / misaligned slices correct.
361fn decode_to_f32(bytes: &[u8], dtype: DType) -> Vec<f32> {
362    match dtype {
363        DType::F32 => {
364            #[cfg(target_endian = "little")]
365            {
366                let (head, mid, tail) = unsafe { bytes.align_to::<f32>() };
367                if head.is_empty() && tail.is_empty() {
368                    return mid.to_vec();
369                }
370            }
371            bytes
372                .as_chunks::<4>()
373                .0
374                .iter()
375                .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
376                .collect()
377        }
378        DType::F16 => {
379            let mut out = vec![0f32; bytes.len() / 2];
380            #[cfg(target_endian = "little")]
381            {
382                // SAFETY: any 2-byte pattern is a valid f16 (no invalid bits);
383                // LE in-memory layout equals the wire's LE f16.
384                let (head, mid, tail) = unsafe { bytes.align_to::<half::f16>() };
385                if head.is_empty() && tail.is_empty() {
386                    mid.convert_to_f32_slice(&mut out);
387                    return out;
388                }
389            }
390            for (o, c) in out.iter_mut().zip(bytes.as_chunks::<2>().0.iter()) {
391                *o = half::f16::from_le_bytes([c[0], c[1]]).to_f32();
392            }
393            out
394        }
395        DType::BF16 => {
396            let mut out = vec![0f32; bytes.len() / 2];
397            #[cfg(target_endian = "little")]
398            {
399                let (head, mid, tail) = unsafe { bytes.align_to::<half::bf16>() };
400                if head.is_empty() && tail.is_empty() {
401                    mid.convert_to_f32_slice(&mut out);
402                    return out;
403                }
404            }
405            for (o, c) in out.iter_mut().zip(bytes.as_chunks::<2>().0.iter()) {
406                *o = half::bf16::from_le_bytes([c[0], c[1]]).to_f32();
407            }
408            out
409        }
410        _ => Vec::new(),
411    }
412}
413
414/// SIMD-batch encode of f32 values **into** `dst` (raw `dtype` bytes), in
415/// place. On little-endian, f16/bf16 convert straight into the reinterpreted
416/// `dst` slice via `half`'s vectorized `convert_from_f32_slice` — no temporary
417/// `Vec<f16>` and no `Vec<u8>`, the two allocations the old encode paid per
418/// ring step. A scalar `to_le_bytes` path keeps BE / misaligned correct.
419fn encode_into(dst: &mut [u8], vals: &[f32], dtype: DType) {
420    match dtype {
421        DType::F32 => {
422            for (c, &v) in dst.as_chunks_mut::<4>().0.iter_mut().zip(vals) {
423                c.copy_from_slice(&v.to_le_bytes());
424            }
425        }
426        DType::F16 => {
427            #[cfg(target_endian = "little")]
428            {
429                let (head, mid, tail) = unsafe { dst.align_to_mut::<half::f16>() };
430                if head.is_empty() && tail.is_empty() {
431                    mid.convert_from_f32_slice(vals);
432                    return;
433                }
434            }
435            for (c, &v) in dst.as_chunks_mut::<2>().0.iter_mut().zip(vals) {
436                c.copy_from_slice(&half::f16::from_f32(v).to_le_bytes());
437            }
438        }
439        DType::BF16 => {
440            #[cfg(target_endian = "little")]
441            {
442                let (head, mid, tail) = unsafe { dst.align_to_mut::<half::bf16>() };
443                if head.is_empty() && tail.is_empty() {
444                    mid.convert_from_f32_slice(vals);
445                    return;
446                }
447            }
448            for (c, &v) in dst.as_chunks_mut::<2>().0.iter_mut().zip(vals) {
449                c.copy_from_slice(&half::bf16::from_f32(v).to_le_bytes());
450            }
451        }
452        _ => {}
453    }
454}
455
456/// Reduce `incoming` into `dst` (both raw `dtype` bytes), elementwise with
457/// `op`. f16/bf16/f32 accumulate in f32 (SIMD codec); f64 stays in f64.
458fn reduce_into(dst: &mut [u8], incoming: &[u8], dtype: DType, op: ReduceKind) {
459    if is_reducible_int(dtype) {
460        let mut acc = decode_to_i32(dst, dtype);
461        let b = decode_to_i32(incoming, dtype);
462        for (x, &y) in acc.iter_mut().zip(&b) {
463            *x = combine_i32(op, *x, y);
464        }
465        encode_into_i32(dst, &acc, dtype);
466    } else if dtype == DType::F64 {
467        let a = decode_to_f64(dst, dtype);
468        let b = decode_to_f64(incoming, dtype);
469        let m: Vec<f64> = a
470            .iter()
471            .zip(&b)
472            .map(|(&x, &y)| combine64(op, x, y))
473            .collect();
474        dst.copy_from_slice(&encode_from_f64(&m, dtype));
475    } else {
476        let mut acc = decode_to_f32(dst, dtype);
477        let b = decode_to_f32(incoming, dtype);
478        for (x, &y) in acc.iter_mut().zip(&b) {
479            *x = combine32(op, *x, y);
480        }
481        encode_into(dst, &acc, dtype);
482    }
483}
484
485/// Divide every element of `data` (raw `dtype` bytes) by `n` — the `Mean`
486/// finalize, applied once over the whole reduced buffer.
487fn scale_mean(data: &mut [u8], dtype: DType, n: usize) {
488    if is_reducible_int(dtype) {
489        // Integer mean (federated averaging): round-to-nearest i32 divide.
490        let half = n as i32 / 2;
491        let mut acc = decode_to_i32(data, dtype);
492        for v in acc.iter_mut() {
493            *v = (*v + if *v >= 0 { half } else { -half }) / n as i32;
494        }
495        encode_into_i32(data, &acc, dtype);
496    } else if dtype == DType::F64 {
497        let scaled: Vec<f64> = decode_to_f64(data, dtype)
498            .iter()
499            .map(|&v| v / n as f64)
500            .collect();
501        data.copy_from_slice(&encode_from_f64(&scaled, dtype));
502    } else {
503        let inv = 1.0 / n as f32;
504        let mut acc = decode_to_f32(data, dtype);
505        for v in acc.iter_mut() {
506            *v *= inv;
507        }
508        encode_into(data, &acc, dtype);
509    }
510}
511
512/// Pick a common reduce dtype for a **mixed-precision** mesh. Reduction
513/// accumulates in f64 internally either way; this only decides the dtype the
514/// ranks agree to exchange when their *local* dtypes differ. F64 if any rank
515/// holds f64, else F32 — the safe accumulator (NCCL's default). Ranks that
516/// share a dtype should just reduce in it natively (smaller wire).
517pub fn negotiate_reduce_dtype(dtypes: &[DType]) -> DType {
518    if dtypes.contains(&DType::F64) {
519        DType::F64
520    } else {
521        DType::F32
522    }
523}
524
525/// A handle to the collective-communication world: a rank, a size, and
526/// the transport that connects them.
527///
528/// Wraps an `Arc<dyn Transport>` so the same group can be cloned into
529/// the generator loop, the weight loader, and (later) per-layer
530/// collective ops without re-establishing connections.
531#[derive(Clone)]
532pub struct ProcessGroup {
533    transport: Arc<dyn Transport>,
534}
535
536impl ProcessGroup {
537    pub fn new(transport: Arc<dyn Transport>) -> Self {
538        Self { transport }
539    }
540
541    pub fn rank(&self) -> u32 {
542        self.transport.rank()
543    }
544
545    pub fn world_size(&self) -> u32 {
546        self.transport.world_size()
547    }
548
549    /// `true` on the lowest rank — the canonical place to print, sample,
550    /// or own the final logits in a pipeline.
551    pub fn is_leader(&self) -> bool {
552        self.rank() == 0
553    }
554
555    pub fn transport(&self) -> &Arc<dyn Transport> {
556        &self.transport
557    }
558
559    pub fn barrier(&self) -> Result<(), CollectiveError> {
560        self.transport.barrier()
561    }
562
563    // ---- point-to-point (pipeline parallel) --------------------------
564
565    /// Send a hidden-state (or any) tensor to `to`. `tag` must be below
566    /// [`TAG_RESERVED_BASE`] (those tags are reserved for collectives).
567    pub fn send_f32(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
568        debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
569        self.send_f32_tagged(to, tag, data)
570    }
571
572    /// Receive a tensor from `from`. Length is taken from the wire.
573    /// Like [`Self::recv_f32`] but gives up after `timeout`, returning
574    /// `Ok(None)`.
575    ///
576    /// For a caller that is waiting on a specific peer and would otherwise
577    /// wait forever: a machine that was unplugged or went to sleep does not
578    /// close its sockets, so a plain `recv` on it never returns. A bound
579    /// turns that into an error that can name the rank that went quiet.
580    pub fn recv_f32_timeout(
581        &self,
582        from: u32,
583        tag: u32,
584        timeout: Duration,
585    ) -> Result<Option<Vec<f32>>, CollectiveError> {
586        debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
587        match self.transport.recv_bytes_timeout(from, tag, timeout)? {
588            Some(b) => Ok(Some(le_bytes_to_f32(&b)?)),
589            None => Ok(None),
590        }
591    }
592
593    pub fn recv_f32(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
594        debug_assert!(tag < TAG_RESERVED_BASE, "tag collides with collective tags");
595        self.recv_f32_tagged(from, tag)
596    }
597
598    /// Internal f32 send with no reserved-tag guard — collectives use
599    /// the reserved tags legitimately.
600    fn send_f32_tagged(&self, to: u32, tag: u32, data: &[f32]) -> Result<(), CollectiveError> {
601        self.transport.send_bytes(to, tag, &f32_to_le_bytes(data))
602    }
603
604    fn recv_f32_tagged(&self, from: u32, tag: u32) -> Result<Vec<f32>, CollectiveError> {
605        le_bytes_to_f32(&self.transport.recv_bytes(from, tag)?)
606    }
607
608    // ---- collectives (tensor parallel) -------------------------------
609
610    /// In-place all-reduce: on return every rank holds
611    /// `op({each rank's input})`. All ranks must pass the same length.
612    ///
613    /// Bandwidth-optimal **ring** all-reduce (reduce-scatter then
614    /// all-gather): every rank moves only `~2·(n-1)/n · len` floats in and
615    /// out, independent of `n`, versus the old gather-to-root that funneled
616    /// `O(n)·len` through rank 0. Deadlock-free over `NetTransport`
617    /// because each rank's background reader thread always drains its
618    /// socket, so a `send` never blocks on a peer that is itself sending.
619    pub fn all_reduce(&self, data: &mut [f32], op: ReduceKind) -> Result<(), CollectiveError> {
620        self.all_reduce_mode(data, op, env_reduce_mode())
621    }
622
623    /// [`Self::all_reduce`] with an explicit [`ReduceMode`] (bypasses the env
624    /// default). Every rank must pass the same mode + length.
625    pub fn all_reduce_mode(
626        &self,
627        data: &mut [f32],
628        op: ReduceKind,
629        mode: ReduceMode,
630    ) -> Result<(), CollectiveError> {
631        let n = self.world_size();
632        if n <= 1 {
633            for v in data.iter_mut() {
634                *v = finalize(op, *v, n.max(1));
635            }
636            return Ok(());
637        }
638        match mode {
639            ReduceMode::Ring => self.all_reduce_ring_f32(data, op),
640            ReduceMode::Deterministic => self.all_reduce_deterministic(data, op),
641        }
642    }
643
644    /// Bandwidth-optimal f32 ring (reduce-scatter → all-gather). The default path.
645    fn all_reduce_ring_f32(&self, data: &mut [f32], op: ReduceKind) -> Result<(), CollectiveError> {
646        let n = self.world_size() as usize;
647        let me = self.rank() as usize;
648        let next = ((me + 1) % n) as u32;
649        let prev = ((me + n - 1) % n) as u32;
650        let len = data.len();
651
652        // reduce-scatter (f32, in place): rank `me` ends owning the fully-reduced
653        // chunk (me+1)%n. Single tag is safe — `prev` sends its 2(n-1) frames in
654        // exactly the FIFO order this rank consumes them.
655        for step in 0..n - 1 {
656            let (ss, se) = ring_chunk(len, n, (me + n - step) % n);
657            let (rs, re) = ring_chunk(len, n, (me + n - step - 1) % 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            for (d, v) in data[rs..re].iter_mut().zip(incoming) {
662                *d = combine(op, *d, v);
663            }
664        }
665        // Finalize the owned chunk once (Mean → ÷n), then all-gather the reduced
666        // chunks around the ring. Dividing per-owner before the gather matches the
667        // deterministic ring and each element is still scaled exactly once.
668        let (os, oe) = ring_chunk(len, n, (me + 1) % n);
669        for v in data[os..oe].iter_mut() {
670            *v = finalize(op, *v, n as u32);
671        }
672        self.ring_all_gather(data, n, me)
673    }
674
675    /// The all-gather half shared by both rings: once each rank owns its finalized
676    /// chunk `(me+1)%n`, walk the chunks around the ring so every rank ends with
677    /// the whole vector. f32 payload — the values are already reduced.
678    fn ring_all_gather(
679        &self,
680        data: &mut [f32],
681        n: usize,
682        me: usize,
683    ) -> Result<(), CollectiveError> {
684        let next = ((me + 1) % n) as u32;
685        let prev = ((me + n - 1) % n) as u32;
686        let len = data.len();
687        for step in 0..n - 1 {
688            let (ss, se) = ring_chunk(len, n, (me + n - step + 1) % n);
689            let (rs, re) = ring_chunk(len, n, (me + n - step) % n);
690            self.send_f32_tagged(next, TAG_ALL_REDUCE, &data[ss..se])?;
691            let incoming = self.recv_f32_tagged(prev, TAG_ALL_REDUCE)?;
692            expect_len(incoming.len(), re - rs)?;
693            data[rs..re].copy_from_slice(&incoming);
694        }
695        Ok(())
696    }
697
698    /// [`ReduceMode::Deterministic`]: the same bandwidth-optimal ring as
699    /// [`Self::all_reduce_ring_f32`], but the **reduce-scatter accumulates in f64**
700    /// so no f32 precision is lost, and the all-gather propagates the already-
701    /// reduced values in f32 (nothing left to lose). The fold order is fixed by the
702    /// algorithm (not by message timing), so the result is bitwise reproducible;
703    /// f64 accumulation makes each element the correctly-rounded exact sum, so it
704    /// is effectively independent of world size. Only the reduce-scatter legs carry
705    /// f64 (≈1.5× the f32 ring's bytes) — the ring's `O((n-1)/n · len)` per rank is
706    /// preserved, unlike a gather-to-root's `O(n·len)`.
707    fn all_reduce_deterministic(
708        &self,
709        data: &mut [f32],
710        op: ReduceKind,
711    ) -> Result<(), CollectiveError> {
712        let n = self.world_size() as usize;
713        let me = self.rank() as usize;
714        let next = ((me + 1) % n) as u32;
715        let prev = ((me + n - 1) % n) as u32;
716        let len = data.len();
717
718        // f64 accumulator seeded with this rank's contribution.
719        let mut acc: Vec<f64> = data.iter().map(|&v| v as f64).collect();
720        // Reused across steps — largest chunk is ⌈len/n⌉ f64 (8 bytes each).
721        let mut send = Vec::with_capacity((len / n + 1) * 8);
722
723        // reduce-scatter in f64: rank `me` ends owning the fully-summed chunk
724        // (me+1)%n. Partials travel as f64 so each add is lossless.
725        for step in 0..n - 1 {
726            let (ss, se) = ring_chunk(len, n, (me + n - step) % n);
727            let (rs, re) = ring_chunk(len, n, (me + n - step - 1) % n);
728            send.clear();
729            send.extend(acc[ss..se].iter().flat_map(|v| v.to_le_bytes()));
730            self.transport.send_bytes(next, TAG_ALL_REDUCE, &send)?;
731            let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
732            expect_len(incoming.len(), (re - rs) * 8)?;
733            for (a, c) in acc[rs..re]
734                .iter_mut()
735                .zip(incoming.as_chunks::<8>().0.iter())
736            {
737                *a = combine64(op, *a, f64::from_le_bytes(*c));
738            }
739        }
740
741        // Finalize the owned chunk (Mean divides once, in f64), narrow to f32,
742        // then reuse the shared f32 all-gather to spread the reduced chunks.
743        let (os, oe) = ring_chunk(len, n, (me + 1) % n);
744        for i in os..oe {
745            data[i] = finalize64(op, acc[i], n as u32) as f32;
746        }
747        self.ring_all_gather(data, n, me)
748    }
749
750    /// In-place all-reduce over **any float dtype** (`F16`/`BF16`/`F32`/`F64`),
751    /// operating on the raw little-endian bytes. The wire carries the *native*
752    /// dtype — an fp16 reduction moves half the bytes of the f32 path, the
753    /// direct bandwidth win of mixed precision — while the reduction itself
754    /// accumulates in f64 so range isn't lost mid-sum. Same bandwidth-optimal
755    /// ring as [`Self::all_reduce`]. `data.len()` must be a multiple of the
756    /// dtype's element size, and every rank must pass the same dtype + length.
757    pub fn all_reduce_typed(
758        &self,
759        data: &mut [u8],
760        dtype: DType,
761        op: ReduceKind,
762    ) -> Result<(), CollectiveError> {
763        if !is_reducible_float(dtype) && !is_reducible_int(dtype) {
764            return Err(CollectiveError::TransportError {
765                reason: format!("all_reduce_typed: unsupported dtype {dtype:?}"),
766            });
767        }
768        let esz = dtype.size_bytes();
769        if esz == 0 || !data.len().is_multiple_of(esz) {
770            return Err(CollectiveError::TransportError {
771                reason: format!(
772                    "all_reduce_typed: {} bytes not a multiple of {esz}",
773                    data.len()
774                ),
775            });
776        }
777        let n = self.world_size();
778        if n <= 1 {
779            return Ok(()); // Mean over 1 rank is identity
780        }
781        let n = n as usize;
782        let me = self.rank() as usize;
783        let next = ((me + 1) % n) as u32;
784        let prev = ((me + n - 1) % n) as u32;
785
786        let elems = data.len() / esz;
787        let base = elems / n;
788        let rem = elems % n;
789        let ebound = |i: usize| (i * base + i.min(rem)) * esz; // byte offset of element-chunk i
790        let chunk = |i: usize| (ebound(i), ebound(i + 1));
791
792        // reduce-scatter (decode→combine in f64→encode back into our chunk)
793        for step in 0..n - 1 {
794            let (ss, se) = chunk((me + n - step) % n);
795            let (rs, re) = chunk((me + n - step - 1) % n);
796            self.transport
797                .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
798            let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
799            if incoming.len() != re - rs {
800                return Err(CollectiveError::LengthMismatch {
801                    expected: re - rs,
802                    got: incoming.len(),
803                });
804            }
805            reduce_into(&mut data[rs..re], &incoming, dtype, op);
806        }
807        // all-gather (raw byte copy — already reduced)
808        for step in 0..n - 1 {
809            let (ss, se) = chunk((me + n - step + 1) % n);
810            let (rs, re) = chunk((me + n - step) % n);
811            self.transport
812                .send_bytes(next, TAG_ALL_REDUCE, &data[ss..se])?;
813            let incoming = self.transport.recv_bytes(prev, TAG_ALL_REDUCE)?;
814            if incoming.len() != re - rs {
815                return Err(CollectiveError::LengthMismatch {
816                    expected: re - rs,
817                    got: incoming.len(),
818                });
819            }
820            data[rs..re].copy_from_slice(&incoming);
821        }
822        if matches!(op, ReduceKind::Mean) {
823            scale_mean(data, dtype, n);
824        }
825        Ok(())
826    }
827
828    /// Non-blocking all-reduce: takes ownership of `data`, runs the
829    /// collective on a background thread, and returns a handle whose
830    /// `join()` yields the reduced vector. This is the building block for
831    /// overlapping gradient communication with backward compute — fire it
832    /// per bucket as gradients are produced, keep computing, then join.
833    pub fn spawn_all_reduce(
834        self: &Arc<Self>,
835        mut data: Vec<f32>,
836        op: ReduceKind,
837    ) -> std::thread::JoinHandle<Vec<f32>> {
838        let g = self.clone();
839        std::thread::spawn(move || {
840            g.all_reduce(&mut data, op).expect("background all_reduce");
841            data
842        })
843    }
844
845    /// All-gather: every rank contributes `local` (same length on every
846    /// rank) and receives the concatenation in rank order, length
847    /// `world_size * local.len()`.
848    pub fn all_gather(&self, local: &[f32]) -> Result<Vec<f32>, CollectiveError> {
849        let n = self.world_size();
850        let len = local.len();
851        if n <= 1 {
852            return Ok(local.to_vec());
853        }
854        if self.rank() == 0 {
855            let mut out = vec![0f32; n as usize * len];
856            out[..len].copy_from_slice(local);
857            for r in 1..n {
858                let chunk = self.recv_f32_tagged(r, TAG_ALL_GATHER)?;
859                if chunk.len() != len {
860                    return Err(CollectiveError::LengthMismatch {
861                        expected: len,
862                        got: chunk.len(),
863                    });
864                }
865                let start = r as usize * len;
866                out[start..start + len].copy_from_slice(&chunk);
867            }
868            for r in 1..n {
869                self.send_f32_tagged(r, TAG_ALL_GATHER, &out)?;
870            }
871            Ok(out)
872        } else {
873            self.send_f32_tagged(0, TAG_ALL_GATHER, local)?;
874            let out = self.recv_f32_tagged(0, TAG_ALL_GATHER)?;
875            if out.len() != n as usize * len {
876                return Err(CollectiveError::LengthMismatch {
877                    expected: n as usize * len,
878                    got: out.len(),
879                });
880            }
881            Ok(out)
882        }
883    }
884
885    /// Broadcast `data` from `root` to every rank. On non-root ranks
886    /// `data` is overwritten; its length must already match the root's.
887    pub fn broadcast(&self, root: u32, data: &mut [f32]) -> Result<(), CollectiveError> {
888        let n = self.world_size();
889        if n <= 1 {
890            return Ok(());
891        }
892        if self.rank() == root {
893            for r in 0..n {
894                if r != root {
895                    self.send_f32_tagged(r, TAG_BROADCAST, data)?;
896                }
897            }
898        } else {
899            let res = self.recv_f32_tagged(root, TAG_BROADCAST)?;
900            if res.len() != data.len() {
901                return Err(CollectiveError::LengthMismatch {
902                    expected: data.len(),
903                    got: res.len(),
904                });
905            }
906            data.copy_from_slice(&res);
907        }
908        Ok(())
909    }
910
911    /// All-to-all: rank `r` splits `data` into `world_size` equal chunks and
912    /// sends chunk `j` to rank `j`; output chunk `j` is what rank `j` sent to
913    /// `r`. Both `data` and the result are `world_size * chunk` f32. This is the
914    /// per-rank chunk-grid transpose — the primitive for MoE expert
915    /// dispatch/combine.
916    ///
917    /// **WideEP:** equal-chunk pad helpers remain valid; prefer
918    /// [`Self::all_to_all_v`] for variable per-destination MoE traffic
919    /// (`rlx_collectives::moe_dispatch` / `moe_combine` Phase 1).
920    pub fn all_to_all(&self, data: &[f32]) -> Result<Vec<f32>, CollectiveError> {
921        let n = self.world_size() as usize;
922        if n <= 1 {
923            return Ok(data.to_vec());
924        }
925        if !data.len().is_multiple_of(n) {
926            return Err(CollectiveError::LengthMismatch {
927                expected: n,
928                got: data.len(),
929            });
930        }
931        let chunk = data.len() / n;
932        let rank = self.rank() as usize;
933        let mut out = vec![0f32; data.len()];
934        out[rank * chunk..(rank + 1) * chunk]
935            .copy_from_slice(&data[rank * chunk..(rank + 1) * chunk]);
936        for p in 0..n {
937            if p == rank {
938                continue;
939            }
940            let send = &data[p * chunk..(p + 1) * chunk];
941            // Deadlock-free pairwise exchange: the lower rank sends first.
942            if rank < p {
943                self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL, send)?;
944                let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL)?;
945                out[p * chunk..(p + 1) * chunk].copy_from_slice(&r);
946            } else {
947                let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL)?;
948                out[p * chunk..(p + 1) * chunk].copy_from_slice(&r);
949                self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL, send)?;
950            }
951        }
952        Ok(out)
953    }
954
955    /// Variable-size all-to-all (WideEP Phase 1).
956    ///
957    /// `send_counts[j]` is the number of `f32` elements destined for rank `j`;
958    /// `data` is the concatenation of those chunks in rank order
959    /// (`sum(send_counts) == data.len()`). Returns `(recv_data, recv_counts)`
960    /// where `recv_counts[j]` is the number of elements received from rank `j`
961    /// and `recv_data` concatenates those chunks in rank order.
962    ///
963    /// Protocol: pairwise exchange of a `u64` count, then the `f32` payload
964    /// (lower rank sends first — same deadlock avoidance as [`Self::all_to_all`]).
965    pub fn all_to_all_v(
966        &self,
967        data: &[f32],
968        send_counts: &[usize],
969    ) -> Result<(Vec<f32>, Vec<usize>), CollectiveError> {
970        let n = self.world_size() as usize;
971        if send_counts.len() != n {
972            return Err(CollectiveError::LengthMismatch {
973                expected: n,
974                got: send_counts.len(),
975            });
976        }
977        let total_send: usize = send_counts.iter().sum();
978        if data.len() != total_send {
979            return Err(CollectiveError::LengthMismatch {
980                expected: total_send,
981                got: data.len(),
982            });
983        }
984        if n <= 1 {
985            return Ok((data.to_vec(), send_counts.to_vec()));
986        }
987
988        let rank = self.rank() as usize;
989        let mut send_off = vec![0usize; n + 1];
990        for i in 0..n {
991            send_off[i + 1] = send_off[i] + send_counts[i];
992        }
993
994        // Phase 1 — exchange element counts.
995        let mut recv_counts = vec![0usize; n];
996        recv_counts[rank] = send_counts[rank];
997        for p in 0..n {
998            if p == rank {
999                continue;
1000            }
1001            let my_count = (send_counts[p] as u64).to_le_bytes();
1002            let their = if rank < p {
1003                self.transport
1004                    .send_bytes(p as u32, TAG_ALL_TO_ALL_V, &my_count)?;
1005                self.transport.recv_bytes(p as u32, TAG_ALL_TO_ALL_V)?
1006            } else {
1007                let r = self.transport.recv_bytes(p as u32, TAG_ALL_TO_ALL_V)?;
1008                self.transport
1009                    .send_bytes(p as u32, TAG_ALL_TO_ALL_V, &my_count)?;
1010                r
1011            };
1012            if their.len() != 8 {
1013                return Err(CollectiveError::TransportError {
1014                    reason: format!(
1015                        "all_to_all_v: count from rank {p} has {} bytes, want 8",
1016                        their.len()
1017                    ),
1018                });
1019            }
1020            recv_counts[p] = u64::from_le_bytes(their[..].try_into().unwrap()) as usize;
1021        }
1022
1023        let mut recv_off = vec![0usize; n + 1];
1024        for i in 0..n {
1025            recv_off[i + 1] = recv_off[i] + recv_counts[i];
1026        }
1027        let mut out = vec![0f32; recv_off[n]];
1028        out[recv_off[rank]..recv_off[rank + 1]]
1029            .copy_from_slice(&data[send_off[rank]..send_off[rank + 1]]);
1030
1031        // Phase 2 — exchange payloads (same tag; sequenced after counts).
1032        for p in 0..n {
1033            if p == rank {
1034                continue;
1035            }
1036            let send = &data[send_off[p]..send_off[p + 1]];
1037            let got = if rank < p {
1038                self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL_V, send)?;
1039                self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL_V)?
1040            } else {
1041                let r = self.recv_f32_tagged(p as u32, TAG_ALL_TO_ALL_V)?;
1042                self.send_f32_tagged(p as u32, TAG_ALL_TO_ALL_V, send)?;
1043                r
1044            };
1045            if got.len() != recv_counts[p] {
1046                return Err(CollectiveError::LengthMismatch {
1047                    expected: recv_counts[p],
1048                    got: got.len(),
1049                });
1050            }
1051            out[recv_off[p]..recv_off[p + 1]].copy_from_slice(&got);
1052        }
1053        Ok((out, recv_counts))
1054    }
1055
1056    /// Reduce to `root`: every rank contributes `data`; afterward only `root`'s
1057    /// buffer holds the reduction (non-root buffers are left unchanged). The
1058    /// transpose of `broadcast`.
1059    pub fn reduce(
1060        &self,
1061        root: u32,
1062        data: &mut [f32],
1063        op: ReduceKind,
1064    ) -> Result<(), CollectiveError> {
1065        let n = self.world_size();
1066        if n <= 1 {
1067            return Ok(());
1068        }
1069        if self.rank() == root {
1070            let mut acc = data.to_vec();
1071            for r in 0..n {
1072                if r == root {
1073                    continue;
1074                }
1075                let other = self.recv_f32_tagged(r, TAG_REDUCE)?;
1076                if other.len() != acc.len() {
1077                    return Err(CollectiveError::LengthMismatch {
1078                        expected: acc.len(),
1079                        got: other.len(),
1080                    });
1081                }
1082                for i in 0..acc.len() {
1083                    acc[i] = op.fold(acc[i], other[i]);
1084                }
1085            }
1086            for v in acc.iter_mut() {
1087                *v = op.finalize(*v, n as usize);
1088            }
1089            data.copy_from_slice(&acc);
1090        } else {
1091            self.send_f32_tagged(root, TAG_REDUCE, data)?;
1092        }
1093        Ok(())
1094    }
1095
1096    /// Collective permute: `perm` is a list of `(src, dst)` pairs — rank `src`'s
1097    /// `data` is delivered to rank `dst`'s output. A rank not named as a `dst`
1098    /// receives zeros. Generalizes ring shifts / rotations (JAX's `ppermute`).
1099    pub fn ppermute(&self, data: &[f32], perm: &[(u32, u32)]) -> Result<Vec<f32>, CollectiveError> {
1100        let rank = self.rank();
1101        let my_dst = perm.iter().find(|&&(s, _)| s == rank).map(|&(_, d)| d);
1102        let my_src = perm.iter().find(|&&(_, d)| d == rank).map(|&(s, _)| s);
1103        // Send our data to its destination (a self-permute needs no network).
1104        if let Some(dst) = my_dst {
1105            if dst != rank {
1106                self.send_f32_tagged(dst, TAG_PPERMUTE, data)?;
1107            }
1108        }
1109        // Our output is whatever our source sent (zeros if we're not a dst).
1110        let out = match my_src {
1111            Some(src) if src == rank => data.to_vec(),
1112            Some(src) => self.recv_f32_tagged(src, TAG_PPERMUTE)?,
1113            None => vec![0f32; data.len()],
1114        };
1115        Ok(out)
1116    }
1117
1118    /// Bounded-staleness federated averaging. Rank 0 collects each rank's
1119    /// vector but **skips any that miss `deadline`**, averages the arrivals
1120    /// (always including its own), and broadcasts the result. Returns the
1121    /// participant count on rank 0 (callers weight / log dropouts; non-roots
1122    /// get the averaged `data` and a count of 0). This is how a slow or
1123    /// offline edge client can't stall a training round.
1124    pub fn federated_average(
1125        &self,
1126        data: &mut [f32],
1127        deadline: Duration,
1128    ) -> Result<usize, CollectiveError> {
1129        let n = self.world_size();
1130        if n <= 1 {
1131            return Ok(1);
1132        }
1133        let elems = data.len();
1134        if self.rank() == 0 {
1135            let mut acc = data.to_vec();
1136            let mut present = 1usize;
1137            let end = Instant::now() + deadline;
1138            for r in 1..n {
1139                let remaining = end.saturating_duration_since(Instant::now());
1140                if let Some(bytes) =
1141                    self.transport
1142                        .recv_bytes_timeout(r, TAG_ALL_REDUCE, remaining)?
1143                {
1144                    let other = le_bytes_to_f32(&bytes)?;
1145                    if other.len() == elems {
1146                        for i in 0..elems {
1147                            acc[i] += other[i];
1148                        }
1149                        present += 1;
1150                    }
1151                }
1152            }
1153            let inv = 1.0 / present as f32;
1154            for v in acc.iter_mut() {
1155                *v *= inv;
1156            }
1157            data.copy_from_slice(&acc);
1158            for r in 1..n {
1159                self.send_f32_tagged(r, TAG_ALL_REDUCE, &acc)?;
1160            }
1161            Ok(present)
1162        } else {
1163            self.send_f32_tagged(0, TAG_ALL_REDUCE, data)?;
1164            let res = self.recv_f32_tagged(0, TAG_ALL_REDUCE)?;
1165            if res.len() == elems {
1166                data.copy_from_slice(&res);
1167            }
1168            Ok(0)
1169        }
1170    }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175    use super::*;
1176    use std::collections::{HashMap, VecDeque};
1177    use std::sync::{Condvar, Mutex};
1178
1179    /// In-process [`Transport`] over shared queues — exercises the
1180    /// `ProcessGroup` collectives without sockets. One shared mailbox
1181    /// keyed by `(dst, src, tag)`; `send` pushes, `recv` blocks-pops.
1182    struct ChannelTransport {
1183        rank: u32,
1184        world: u32,
1185        mailbox: Arc<(Mutex<HashMap<(u32, u32, u32), VecDeque<Vec<u8>>>>, Condvar)>,
1186    }
1187
1188    impl ChannelTransport {
1189        fn fan_out(world: u32) -> Vec<Arc<ChannelTransport>> {
1190            let mailbox = Arc::new((Mutex::new(HashMap::new()), Condvar::new()));
1191            (0..world)
1192                .map(|r| {
1193                    Arc::new(ChannelTransport {
1194                        rank: r,
1195                        world,
1196                        mailbox: mailbox.clone(),
1197                    })
1198                })
1199                .collect()
1200        }
1201    }
1202
1203    impl Transport for ChannelTransport {
1204        fn rank(&self) -> u32 {
1205            self.rank
1206        }
1207        fn world_size(&self) -> u32 {
1208            self.world
1209        }
1210        fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError> {
1211            let (m, cv) = &*self.mailbox;
1212            m.lock()
1213                .unwrap()
1214                .entry((to, self.rank, tag))
1215                .or_default()
1216                .push_back(bytes.to_vec());
1217            cv.notify_all();
1218            Ok(())
1219        }
1220        fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError> {
1221            let (m, cv) = &*self.mailbox;
1222            let mut guard = m.lock().unwrap();
1223            loop {
1224                if let Some(q) = guard.get_mut(&(self.rank, from, tag))
1225                    && let Some(v) = q.pop_front()
1226                {
1227                    return Ok(v);
1228                }
1229                guard = cv.wait(guard).unwrap();
1230            }
1231        }
1232    }
1233
1234    fn run_ranks<F>(world: u32, body: F) -> Vec<()>
1235    where
1236        F: Fn(ProcessGroup) + Send + Sync + 'static,
1237    {
1238        let ts = ChannelTransport::fan_out(world);
1239        let body = Arc::new(body);
1240        let handles: Vec<_> = ts
1241            .into_iter()
1242            .map(|t| {
1243                let body = body.clone();
1244                std::thread::spawn(move || body(ProcessGroup::new(t)))
1245            })
1246            .collect();
1247        handles.into_iter().map(|h| h.join().unwrap()).collect()
1248    }
1249
1250    #[test]
1251    fn all_reduce_sum_matches_serial() {
1252        run_ranks(4, |g| {
1253            let r = g.rank() as f32;
1254            let mut data = vec![r + 1.0; 3]; // ranks hold 1,2,3,4
1255            g.all_reduce(&mut data, ReduceKind::Sum).unwrap();
1256            assert_eq!(data, vec![10.0; 3], "rank {}", g.rank());
1257        });
1258    }
1259
1260    #[test]
1261    fn all_reduce_modes_agree_on_the_sum() {
1262        // Ring (f32) and Deterministic (f64 ring) must agree on a clean sum.
1263        for mode in [ReduceMode::Ring, ReduceMode::Deterministic] {
1264            run_ranks(4, move |g| {
1265                let r = g.rank() as f32 + 1.0; // 1,2,3,4
1266                let mut data = vec![r, r * 2.0, r * 3.0];
1267                g.all_reduce_mode(&mut data, ReduceKind::Sum, mode).unwrap();
1268                assert_eq!(
1269                    data,
1270                    vec![10.0, 20.0, 30.0],
1271                    "mode {mode:?} rank {}",
1272                    g.rank()
1273                );
1274            });
1275        }
1276    }
1277
1278    #[test]
1279    fn deterministic_reduce_mean_divides() {
1280        run_ranks(4, |g| {
1281            let mut data = vec![g.rank() as f32 + 1.0; 2]; // 1,2,3,4
1282            g.all_reduce_mode(&mut data, ReduceKind::Mean, ReduceMode::Deterministic)
1283                .unwrap();
1284            assert_eq!(data, vec![2.5, 2.5], "rank {}", g.rank());
1285        });
1286    }
1287
1288    #[test]
1289    fn deterministic_is_exact_where_the_f32_ring_would_lose_precision() {
1290        // rank0=+1e8, rank1=-1e8, ranks2,3=+4 → true sum 8. f32's ulp at 1e8 is 8,
1291        // so a naïve f32 fold of the small terms into 1e8 drops them; the f64
1292        // reduce-scatter keeps full precision and lands exactly on 8 — at ring
1293        // bandwidth (no precision loss for the determinism).
1294        run_ranks(4, |g| {
1295            let v = match g.rank() {
1296                0 => 1e8f32,
1297                1 => -1e8f32,
1298                _ => 4.0f32,
1299            };
1300            let mut data = vec![v, v]; // >1 element so chunking is non-trivial
1301            g.all_reduce_mode(&mut data, ReduceKind::Sum, ReduceMode::Deterministic)
1302                .unwrap();
1303            assert_eq!(data, vec![8.0, 8.0], "rank {}", g.rank());
1304        });
1305    }
1306
1307    #[test]
1308    fn deterministic_is_world_size_independent_for_replicated_input() {
1309        // Every rank contributes the same vector `v`; the reduced Mean must be `v`
1310        // exactly, for ANY world size (2, 3, 5, 7) — the property the f32 ring
1311        // lacks (its last ulp shifts with n). Node-count reproducibility.
1312        for &n in &[2u32, 3, 5, 7] {
1313            run_ranks(n, |g| {
1314                let mut data = vec![0.1f32, 0.2, 0.3, 0.7];
1315                g.all_reduce_mode(&mut data, ReduceKind::Mean, ReduceMode::Deterministic)
1316                    .unwrap();
1317                assert_eq!(
1318                    data,
1319                    vec![0.1, 0.2, 0.3, 0.7],
1320                    "n={} rank {}",
1321                    g.world_size(),
1322                    g.rank()
1323                );
1324            });
1325        }
1326    }
1327
1328    #[test]
1329    fn all_gather_concatenates_in_rank_order() {
1330        run_ranks(3, |g| {
1331            let r = g.rank() as f32;
1332            let out = g.all_gather(&[10.0 * r, 10.0 * r + 1.0]).unwrap();
1333            assert_eq!(
1334                out,
1335                vec![0.0, 1.0, 10.0, 11.0, 20.0, 21.0],
1336                "rank {}",
1337                g.rank()
1338            );
1339        });
1340    }
1341
1342    #[test]
1343    fn broadcast_from_root_overwrites() {
1344        run_ranks(4, |g| {
1345            let mut data = if g.is_leader() {
1346                vec![7.0, 8.0, 9.0]
1347            } else {
1348                vec![0.0, 0.0, 0.0]
1349            };
1350            g.broadcast(0, &mut data).unwrap();
1351            assert_eq!(data, vec![7.0, 8.0, 9.0], "rank {}", g.rank());
1352        });
1353    }
1354
1355    #[test]
1356    fn all_to_all_v_variable_chunks() {
1357        // Every rank uses send_counts[j] = j+1. Rank r therefore receives
1358        // from each peer a chunk of length (r+1) filled with (100*peer + r).
1359        run_ranks(3, |g| {
1360            let n = g.world_size() as usize;
1361            let r = g.rank() as usize;
1362            let send_counts: Vec<usize> = (0..n).map(|j| j + 1).collect();
1363            let mut data = Vec::new();
1364            for (j, &c) in send_counts.iter().enumerate() {
1365                for _ in 0..c {
1366                    data.push((100 * r + j) as f32);
1367                }
1368            }
1369            let (recv, recv_counts) = g.all_to_all_v(&data, &send_counts).unwrap();
1370            assert_eq!(recv_counts, vec![r + 1; n], "rank {r} recv_counts");
1371            let mut off = 0usize;
1372            for (j, &c) in recv_counts.iter().enumerate() {
1373                let chunk = &recv[off..off + c];
1374                assert!(
1375                    chunk
1376                        .iter()
1377                        .all(|&v| (v - (100 * j + r) as f32).abs() < 1e-6),
1378                    "rank {r} from {j}: {chunk:?}"
1379                );
1380                off += c;
1381            }
1382        });
1383    }
1384
1385    #[test]
1386    fn barrier_round_trips() {
1387        run_ranks(4, |g| {
1388            g.barrier().unwrap();
1389            g.barrier().unwrap(); // repeated barriers stay in lockstep
1390        });
1391    }
1392
1393    #[test]
1394    fn point_to_point_ring_handoff() {
1395        // Pipeline-style: each rank sends its id to the next, lowest
1396        // rank closes the ring. Verifies tagged matched send/recv.
1397        run_ranks(3, |g| {
1398            let n = g.world_size();
1399            let me = g.rank();
1400            let next = (me + 1) % n;
1401            let prev = (me + n - 1) % n;
1402            // Stagger to avoid all-send-then-all-recv lockstep issues:
1403            // even ranks send first, odd ranks recv first.
1404            if me % 2 == 0 {
1405                g.send_f32(next, 1, &[me as f32]).unwrap();
1406                let got = g.recv_f32(prev, 1).unwrap();
1407                assert_eq!(got, vec![prev as f32]);
1408            } else {
1409                let got = g.recv_f32(prev, 1).unwrap();
1410                assert_eq!(got, vec![prev as f32]);
1411                g.send_f32(next, 1, &[me as f32]).unwrap();
1412            }
1413        });
1414    }
1415
1416    fn f16_bytes(vals: &[f32]) -> Vec<u8> {
1417        vals.iter()
1418            .flat_map(|&v| half::f16::from_f32(v).to_le_bytes())
1419            .collect()
1420    }
1421    fn f16_vals(bytes: &[u8]) -> Vec<f32> {
1422        bytes
1423            .as_chunks::<2>()
1424            .0
1425            .iter()
1426            .map(|c| half::f16::from_le_bytes([c[0], c[1]]).to_f32())
1427            .collect()
1428    }
1429
1430    #[test]
1431    fn all_reduce_typed_f16_sums_across_ranks() {
1432        run_ranks(4, |g| {
1433            let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 3]); // 1,2,3,4
1434            g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1435                .unwrap();
1436            assert_eq!(f16_vals(&bytes), vec![10.0; 3], "rank {}", g.rank());
1437        });
1438    }
1439
1440    #[test]
1441    fn all_reduce_typed_bf16_mean() {
1442        run_ranks(4, |g| {
1443            let mut bytes: Vec<u8> = [g.rank() as f32 + 1.0; 4]
1444                .iter()
1445                .flat_map(|&v| half::bf16::from_f32(v).to_le_bytes())
1446                .collect();
1447            g.all_reduce_typed(&mut bytes, DType::BF16, ReduceKind::Mean)
1448                .unwrap();
1449            let got: Vec<f32> = bytes
1450                .as_chunks::<2>()
1451                .0
1452                .iter()
1453                .map(|c| half::bf16::from_le_bytes([c[0], c[1]]).to_f32())
1454                .collect();
1455            // mean(1,2,3,4) = 2.5, exactly representable in bf16
1456            assert_eq!(got, vec![2.5; 4], "rank {}", g.rank());
1457        });
1458    }
1459
1460    #[test]
1461    fn all_reduce_typed_uneven_length() {
1462        // 5 elems over 4 ranks exercises the remainder chunking.
1463        run_ranks(4, |g| {
1464            let mut bytes = f16_bytes(&[g.rank() as f32 + 1.0; 5]);
1465            g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1466                .unwrap();
1467            assert_eq!(f16_vals(&bytes), vec![10.0; 5], "rank {}", g.rank());
1468        });
1469    }
1470
1471    #[test]
1472    fn all_reduce_typed_f16_large_buffer_simd_path() {
1473        // 1000 elems is past the SIMD-batch threshold, so this exercises the
1474        // vectorized `convert_*_f32_slice` path (small tests use it too, but
1475        // this guards the batched conversion against off-by-one regressions).
1476        run_ranks(3, |g| {
1477            let mut bytes = f16_bytes(&vec![g.rank() as f32 + 1.0; 1000]); // 1,2,3
1478            g.all_reduce_typed(&mut bytes, DType::F16, ReduceKind::Sum)
1479                .unwrap();
1480            assert_eq!(f16_vals(&bytes), vec![6.0; 1000], "rank {}", g.rank());
1481        });
1482    }
1483
1484    #[test]
1485    fn all_reduce_typed_i8_federated_mean() {
1486        // The edge case: three int8 "clients" averaged (FedAvg-style).
1487        run_ranks(3, |g| {
1488            let mut bytes = vec![((g.rank() + 1) * 10) as i8 as u8; 4]; // 10,20,30
1489            g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Mean)
1490                .unwrap();
1491            let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1492            assert_eq!(got, vec![20i8; 4], "rank {}", g.rank()); // mean = 20
1493        });
1494    }
1495
1496    #[test]
1497    fn all_reduce_typed_i8_sum_saturates() {
1498        run_ranks(2, |g| {
1499            let mut bytes = vec![100i8 as u8; 2];
1500            g.all_reduce_typed(&mut bytes, DType::I8, ReduceKind::Sum)
1501                .unwrap();
1502            let got: Vec<i8> = bytes.iter().map(|&b| b as i8).collect();
1503            assert_eq!(got, vec![127i8; 2], "rank {}", g.rank()); // 200 clamps to i8::MAX
1504        });
1505    }
1506
1507    #[test]
1508    fn negotiate_reduce_dtype_rules() {
1509        assert_eq!(
1510            negotiate_reduce_dtype(&[DType::F16, DType::BF16]),
1511            DType::F32
1512        );
1513        assert_eq!(
1514            negotiate_reduce_dtype(&[DType::F16, DType::F64]),
1515            DType::F64
1516        );
1517        assert_eq!(negotiate_reduce_dtype(&[]), DType::F32);
1518    }
1519}