Skip to main content

rlx_driver/
transport.rs

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