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        .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
165/// f64 mean-finalize for the deterministic reducer (divide once, in f64).
166fn finalize64(op: ReduceKind, acc: f64, n: u32) -> f64 {
167    match op {
168        ReduceKind::Mean => acc / (n as f64),
169        _ => acc,
170    }
171}
172
173/// `[start, end)` of chunk `i` in a near-equal `n`-way split of `len` — the
174/// first `len % n` chunks get one extra element. The single source of truth for
175/// every ring collective's chunk boundaries (both [`ProcessGroup::all_reduce`]
176/// rings partition the vector this way, and rank `me` ends up owning chunk
177/// `(me + 1) % n`).
178fn 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
185/// Guard that a received frame is the length the ring expects for this step.
186fn 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/// Reduction algorithm + precision for [`ProcessGroup::all_reduce`]. Selected
197/// per call, or globally via env (see [`env_reduce_mode`]).
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum ReduceMode {
200    /// Bandwidth-optimal ring, **f32** accumulation. Fastest; f32 precision, and
201    /// the last-ulp result depends on world size. Reproducible run-to-run. Default.
202    Ring,
203    /// **Deterministic + precise, at ring speed.** The same bandwidth-optimal ring,
204    /// but its reduce-scatter **accumulates in f64** (the all-gather stays f32 —
205    /// those values are already reduced, so there is no precision left to lose).
206    /// The fold order is fixed by the algorithm, so the result is bitwise
207    /// reproducible run-to-run; f64 accumulation makes each element the
208    /// correctly-rounded exact cross-rank sum, so it neither loses precision nor
209    /// biases the gradient and is effectively independent of world size. Only the
210    /// reduce-scatter carries an f64 payload (≈1.5× the f32 ring's bytes) — NOT a
211    /// gather-to-root's `O(n·len)`, so speed is preserved. The mode to pick when
212    /// you want reproducible, precise data-parallel training.
213    Deterministic,
214}
215
216/// Global reduction mode from the environment (read once), so a whole cluster
217/// opts into deterministic + precise reductions with no code change:
218///   `RLX_DETERMINISTIC_REDUCE=1` → [`ReduceMode::Deterministic`]
219///   (unset)                      → [`ReduceMode::Ring`]
220/// **Every rank must agree** (mismatched modes mix incompatible message sizes and
221/// would desync), so set it on all nodes — the same discipline as any other
222/// cluster-wide `RLX_*` flag.
223pub 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
239/// f64 reduction step — collectives accumulate in f64 regardless of the
240/// payload dtype, so f16/bf16 reductions don't lose range mid-sum.
241fn 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
249/// The float dtypes a collective can reduce over the wire.
250fn is_reducible_float(d: DType) -> bool {
251    matches!(d, DType::F16 | DType::BF16 | DType::F32 | DType::F64)
252}
253
254/// The integer dtypes a collective can reduce — the edge/quantized tier
255/// (MCU/FPGA/ANE). Accumulated in i32 so a sum of 8-bit lanes can't overflow.
256fn 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
292/// Decode `bytes` of `dtype` to f64 for reduction. Little-endian on the wire.
293fn 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
315/// Encode f64 reduction results back to `dtype` bytes (little-endian).
316fn 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
336/// f32 reduction step — the accumulator for f16/bf16/f32. f32 is exact for
337/// f16/bf16 inputs and matches rlx's native f32 collective bit-for-bit.
338fn 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
346/// SIMD-batch decode of `dtype` bytes to f32. On little-endian targets (Apple
347/// Silicon + x86) f16/bf16 reinterpret in place and `half`'s vectorized
348/// `convert_to_f32_slice` runs ~an order of magnitude faster than the
349/// per-element scalar path; a scalar `from_le_bytes` fallback keeps big-endian
350/// / misaligned slices correct.
351fn 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                // SAFETY: any 2-byte pattern is a valid f16 (no invalid bits);
371                // LE in-memory layout equals the wire's LE f16.
372                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
402/// SIMD-batch encode of f32 values **into** `dst` (raw `dtype` bytes), in
403/// place. On little-endian, f16/bf16 convert straight into the reinterpreted
404/// `dst` slice via `half`'s vectorized `convert_from_f32_slice` — no temporary
405/// `Vec<f16>` and no `Vec<u8>`, the two allocations the old encode paid per
406/// ring step. A scalar `to_le_bytes` path keeps BE / misaligned correct.
407fn 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
444/// Reduce `incoming` into `dst` (both raw `dtype` bytes), elementwise with
445/// `op`. f16/bf16/f32 accumulate in f32 (SIMD codec); f64 stays in f64.
446fn 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
473/// Divide every element of `data` (raw `dtype` bytes) by `n` — the `Mean`
474/// finalize, applied once over the whole reduced buffer.
475fn scale_mean(data: &mut [u8], dtype: DType, n: usize) {
476    if is_reducible_int(dtype) {
477        // Integer mean (federated averaging): round-to-nearest i32 divide.
478        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
500/// Pick a common reduce dtype for a **mixed-precision** mesh. Reduction
501/// accumulates in f64 internally either way; this only decides the dtype the
502/// ranks agree to exchange when their *local* dtypes differ. F64 if any rank
503/// holds f64, else F32 — the safe accumulator (NCCL's default). Ranks that
504/// share a dtype should just reduce in it natively (smaller wire).
505pub 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/// A handle to the collective-communication world: a rank, a size, and
514/// the transport that connects them.
515///
516/// Wraps an `Arc<dyn Transport>` so the same group can be cloned into
517/// the generator loop, the weight loader, and (later) per-layer
518/// collective ops without re-establishing connections.
519#[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    /// `true` on the lowest rank — the canonical place to print, sample,
538    /// or own the final logits in a pipeline.
539    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    // ---- point-to-point (pipeline parallel) --------------------------
552
553    /// Send a hidden-state (or any) tensor to `to`. `tag` must be below
554    /// [`TAG_RESERVED_BASE`] (those tags are reserved for collectives).
555    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    /// Receive a tensor from `from`. Length is taken from the wire.
561    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    /// Internal f32 send with no reserved-tag guard — collectives use
567    /// the reserved tags legitimately.
568    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    // ---- collectives (tensor parallel) -------------------------------
577
578    /// In-place all-reduce: on return every rank holds
579    /// `op({each rank's input})`. All ranks must pass the same length.
580    ///
581    /// Bandwidth-optimal **ring** all-reduce (reduce-scatter then
582    /// all-gather): every rank moves only `~2·(n-1)/n · len` floats in and
583    /// out, independent of `n`, versus the old gather-to-root that funneled
584    /// `O(n)·len` through rank 0. Deadlock-free over `NetTransport`
585    /// because each rank's background reader thread always drains its
586    /// socket, so a `send` never blocks on a peer that is itself sending.
587    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    /// [`Self::all_reduce`] with an explicit [`ReduceMode`] (bypasses the env
592    /// default). Every rank must pass the same mode + length.
593    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    /// Bandwidth-optimal f32 ring (reduce-scatter → all-gather). The default path.
613    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        // reduce-scatter (f32, in place): rank `me` ends owning the fully-reduced
621        // chunk (me+1)%n. Single tag is safe — `prev` sends its 2(n-1) frames in
622        // exactly the FIFO order this rank consumes them.
623        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        // Finalize the owned chunk once (Mean → ÷n), then all-gather the reduced
634        // chunks around the ring. Dividing per-owner before the gather matches the
635        // deterministic ring and each element is still scaled exactly once.
636        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    /// The all-gather half shared by both rings: once each rank owns its finalized
644    /// chunk `(me+1)%n`, walk the chunks around the ring so every rank ends with
645    /// the whole vector. f32 payload — the values are already reduced.
646    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    /// [`ReduceMode::Deterministic`]: the same bandwidth-optimal ring as
667    /// [`Self::all_reduce_ring_f32`], but the **reduce-scatter accumulates in f64**
668    /// so no f32 precision is lost, and the all-gather propagates the already-
669    /// reduced values in f32 (nothing left to lose). The fold order is fixed by the
670    /// algorithm (not by message timing), so the result is bitwise reproducible;
671    /// f64 accumulation makes each element the correctly-rounded exact sum, so it
672    /// is effectively independent of world size. Only the reduce-scatter legs carry
673    /// f64 (≈1.5× the f32 ring's bytes) — the ring's `O((n-1)/n · len)` per rank is
674    /// preserved, unlike a gather-to-root's `O(n·len)`.
675    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        // f64 accumulator seeded with this rank's contribution.
687        let mut acc: Vec<f64> = data.iter().map(|&v| v as f64).collect();
688        // Reused across steps — largest chunk is ⌈len/n⌉ f64 (8 bytes each).
689        let mut send = Vec::with_capacity((len / n + 1) * 8);
690
691        // reduce-scatter in f64: rank `me` ends owning the fully-summed chunk
692        // (me+1)%n. Partials travel as f64 so each add is lossless.
693        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        // Finalize the owned chunk (Mean divides once, in f64), narrow to f32,
707        // then reuse the shared f32 all-gather to spread the reduced chunks.
708        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    /// In-place all-reduce over **any float dtype** (`F16`/`BF16`/`F32`/`F64`),
716    /// operating on the raw little-endian bytes. The wire carries the *native*
717    /// dtype — an fp16 reduction moves half the bytes of the f32 path, the
718    /// direct bandwidth win of mixed precision — while the reduction itself
719    /// accumulates in f64 so range isn't lost mid-sum. Same bandwidth-optimal
720    /// ring as [`Self::all_reduce`]. `data.len()` must be a multiple of the
721    /// dtype's element size, and every rank must pass the same dtype + length.
722    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(()); // Mean over 1 rank is identity
745        }
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; // byte offset of element-chunk i
755        let chunk = |i: usize| (ebound(i), ebound(i + 1));
756
757        // reduce-scatter (decode→combine in f64→encode back into our chunk)
758        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        // all-gather (raw byte copy — already reduced)
773        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    /// Non-blocking all-reduce: takes ownership of `data`, runs the
794    /// collective on a background thread, and returns a handle whose
795    /// `join()` yields the reduced vector. This is the building block for
796    /// overlapping gradient communication with backward compute — fire it
797    /// per bucket as gradients are produced, keep computing, then join.
798    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    /// All-gather: every rank contributes `local` (same length on every
811    /// rank) and receives the concatenation in rank order, length
812    /// `world_size * local.len()`.
813    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    /// Broadcast `data` from `root` to every rank. On non-root ranks
851    /// `data` is overwritten; its length must already match the root's.
852    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    /// All-to-all: rank `r` splits `data` into `world_size` equal chunks and
877    /// sends chunk `j` to rank `j`; output chunk `j` is what rank `j` sent to
878    /// `r`. Both `data` and the result are `world_size * chunk` f32. This is the
879    /// per-rank chunk-grid transpose — the primitive for MoE expert
880    /// dispatch/combine.
881    ///
882    /// **WideEP:** equal-chunk pad helpers remain valid; prefer
883    /// [`Self::all_to_all_v`] for variable per-destination MoE traffic
884    /// (`rlx_collectives::moe_dispatch` / `moe_combine` Phase 1).
885    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            // Deadlock-free pairwise exchange: the lower rank sends first.
907            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    /// Variable-size all-to-all (WideEP Phase 1).
921    ///
922    /// `send_counts[j]` is the number of `f32` elements destined for rank `j`;
923    /// `data` is the concatenation of those chunks in rank order
924    /// (`sum(send_counts) == data.len()`). Returns `(recv_data, recv_counts)`
925    /// where `recv_counts[j]` is the number of elements received from rank `j`
926    /// and `recv_data` concatenates those chunks in rank order.
927    ///
928    /// Protocol: pairwise exchange of a `u64` count, then the `f32` payload
929    /// (lower rank sends first — same deadlock avoidance as [`Self::all_to_all`]).
930    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        // Phase 1 — exchange element counts.
960        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        // Phase 2 — exchange payloads (same tag; sequenced after counts).
997        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    /// Reduce to `root`: every rank contributes `data`; afterward only `root`'s
1022    /// buffer holds the reduction (non-root buffers are left unchanged). The
1023    /// transpose of `broadcast`.
1024    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    /// Collective permute: `perm` is a list of `(src, dst)` pairs — rank `src`'s
1062    /// `data` is delivered to rank `dst`'s output. A rank not named as a `dst`
1063    /// receives zeros. Generalizes ring shifts / rotations (JAX's `ppermute`).
1064    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        // Send our data to its destination (a self-permute needs no network).
1069        if let Some(dst) = my_dst {
1070            if dst != rank {
1071                self.send_f32_tagged(dst, TAG_PPERMUTE, data)?;
1072            }
1073        }
1074        // Our output is whatever our source sent (zeros if we're not a dst).
1075        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    /// Bounded-staleness federated averaging. Rank 0 collects each rank's
1084    /// vector but **skips any that miss `deadline`**, averages the arrivals
1085    /// (always including its own), and broadcasts the result. Returns the
1086    /// participant count on rank 0 (callers weight / log dropouts; non-roots
1087    /// get the averaged `data` and a count of 0). This is how a slow or
1088    /// offline edge client can't stall a training round.
1089    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    /// In-process [`Transport`] over shared queues — exercises the
1145    /// `ProcessGroup` collectives without sockets. One shared mailbox
1146    /// keyed by `(dst, src, tag)`; `send` pushes, `recv` blocks-pops.
1147    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]; // ranks hold 1,2,3,4
1220            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        // Ring (f32) and Deterministic (f64 ring) must agree on a clean sum.
1228        for mode in [ReduceMode::Ring, ReduceMode::Deterministic] {
1229            run_ranks(4, move |g| {
1230                let r = g.rank() as f32 + 1.0; // 1,2,3,4
1231                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]; // 1,2,3,4
1247            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        // rank0=+1e8, rank1=-1e8, ranks2,3=+4 → true sum 8. f32's ulp at 1e8 is 8,
1256        // so a naïve f32 fold of the small terms into 1e8 drops them; the f64
1257        // reduce-scatter keeps full precision and lands exactly on 8 — at ring
1258        // bandwidth (no precision loss for the determinism).
1259        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]; // >1 element so chunking is non-trivial
1266            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        // Every rank contributes the same vector `v`; the reduced Mean must be `v`
1275        // exactly, for ANY world size (2, 3, 5, 7) — the property the f32 ring
1276        // lacks (its last ulp shifts with n). Node-count reproducibility.
1277        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        // Every rank uses send_counts[j] = j+1. Rank r therefore receives
1323        // from each peer a chunk of length (r+1) filled with (100*peer + r).
1324        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(); // repeated barriers stay in lockstep
1355        });
1356    }
1357
1358    #[test]
1359    fn point_to_point_ring_handoff() {
1360        // Pipeline-style: each rank sends its id to the next, lowest
1361        // rank closes the ring. Verifies tagged matched send/recv.
1362        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            // Stagger to avoid all-send-then-all-recv lockstep issues:
1368            // even ranks send first, odd ranks recv first.
1369            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]); // 1,2,3,4
1397            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            // mean(1,2,3,4) = 2.5, exactly representable in bf16
1417            assert_eq!(got, vec![2.5; 4], "rank {}", g.rank());
1418        });
1419    }
1420
1421    #[test]
1422    fn all_reduce_typed_uneven_length() {
1423        // 5 elems over 4 ranks exercises the remainder chunking.
1424        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        // 1000 elems is past the SIMD-batch threshold, so this exercises the
1435        // vectorized `convert_*_f32_slice` path (small tests use it too, but
1436        // this guards the batched conversion against off-by-one regressions).
1437        run_ranks(3, |g| {
1438            let mut bytes = f16_bytes(&vec![g.rank() as f32 + 1.0; 1000]); // 1,2,3
1439            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        // The edge case: three int8 "clients" averaged (FedAvg-style).
1448        run_ranks(3, |g| {
1449            let mut bytes = vec![((g.rank() + 1) * 10) as i8 as u8; 4]; // 10,20,30
1450            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()); // mean = 20
1454        });
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()); // 200 clamps to i8::MAX
1465        });
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}