Skip to main content

rlx_driver/
net.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//! Networked multi-node transport (plan #12/#49, multi-node).
6//!
7//! [`NetTransport`] is one TCP connection per rank pair (full mesh)
8//! with a single reader thread per connection that *demultiplexes*
9//! every incoming frame into the right place:
10//!
11//!   - `SEND`    → a two-sided inbox keyed by `(src, tag)` that
12//!     [`Transport::recv_bytes`] pops (pipeline-parallel hidden states).
13//!   - `PUT`     → this rank's symmetric heap (one-sided write).
14//!   - `GETREQ`  → read this rank's heap, reply with `GETRESP`.
15//!   - `GETRESP` → a response inbox the blocked
16//!     [`SymmetricTransport::get`] pops.
17//!
18//! Because the reader thread services the socket, a blocked `recv`/`get`
19//! on the main thread never stalls the peer — so the gather-to-root
20//! collectives in [`crate::transport::ProcessGroup`] (and the
21//! one-sided collectives in [`crate::collective`]) are deadlock-free
22//! over any rank count.
23//!
24//! Two public constructors expose the same engine with different
25//! intent:
26//!   - [`TcpTransport::bind`] — portable; give it any reachable IPs
27//!     (Ethernet, or the macOS Thunderbolt Bridge link).
28//!   - [`ThunderboltTransport::bind`] — same wire protocol, intended
29//!     for IPs on the Thunderbolt interface, and the place a future
30//!     zero-copy DMA backend slots in behind the unchanged
31//!     [`Transport`] + [`SymmetricTransport`] traits.
32
33use crate::symmetric::{CollectiveError, Rank, SymmetricBuffer, SymmetricTransport};
34use crate::transport::{Transport, default_barrier};
35use std::collections::{HashMap, VecDeque};
36use std::io::{self, Read, Write};
37use std::net::{IpAddr, Shutdown, SocketAddr, TcpListener, TcpStream};
38use std::sync::atomic::{AtomicBool, Ordering};
39use std::sync::{Arc, Condvar, Mutex, RwLock};
40use std::thread::{self, JoinHandle};
41use std::time::{Duration, Instant};
42
43// Frame kinds. Header is fixed 13 bytes: [kind:u8][a:u32 le][b:u32 le][len:u32 le].
44const HELLO: u8 = 1; // a = connector rank, no payload
45const SEND: u8 = 2; // a = src rank, b = tag, payload = bytes
46const PUT: u8 = 3; // a = src rank, b = offset, payload = bytes to write
47const GETREQ: u8 = 4; // a = requester rank, payload = [offset:u32 le][len:u32 le]
48const GETRESP: u8 = 5; // a = responder rank, payload = bytes
49
50const HEADER_LEN: usize = 13;
51
52fn write_frame<W: Write>(w: &mut W, kind: u8, a: u32, b: u32, payload: &[u8]) -> io::Result<()> {
53    let mut hdr = [0u8; HEADER_LEN];
54    hdr[0] = kind;
55    hdr[1..5].copy_from_slice(&a.to_le_bytes());
56    hdr[5..9].copy_from_slice(&b.to_le_bytes());
57    hdr[9..13].copy_from_slice(&(payload.len() as u32).to_le_bytes());
58    // Coalesce header + payload into ONE write for the small frames that
59    // dominate collective latency: with TCP_NODELAY set, two separate
60    // writes became two segments and an extra RTT-coupled syscall. For
61    // large frames the second write is negligible against the payload, so
62    // keep them separate and skip the copy.
63    const COALESCE_MAX: usize = 1 << 16;
64    if payload.len() <= COALESCE_MAX {
65        let mut buf = Vec::with_capacity(HEADER_LEN + payload.len());
66        buf.extend_from_slice(&hdr);
67        buf.extend_from_slice(payload);
68        w.write_all(&buf)?;
69    } else {
70        w.write_all(&hdr)?;
71        w.write_all(payload)?;
72    }
73    w.flush()
74}
75
76fn read_frame<R: Read>(r: &mut R) -> io::Result<(u8, u32, u32, Vec<u8>)> {
77    let mut hdr = [0u8; HEADER_LEN];
78    r.read_exact(&mut hdr)?;
79    let kind = hdr[0];
80    let a = u32::from_le_bytes(hdr[1..5].try_into().unwrap());
81    let b = u32::from_le_bytes(hdr[5..9].try_into().unwrap());
82    let len = u32::from_le_bytes(hdr[9..13].try_into().unwrap()) as usize;
83    let mut payload = vec![0u8; len];
84    if len > 0 {
85        r.read_exact(&mut payload)?;
86    }
87    Ok((kind, a, b, payload))
88}
89
90fn connect_retry(addr: SocketAddr) -> io::Result<TcpStream> {
91    let mut last: Option<io::Error> = None;
92    for _ in 0..400 {
93        match TcpStream::connect(addr) {
94            Ok(s) => return Ok(s),
95            Err(e) => {
96                last = Some(e);
97                thread::sleep(Duration::from_millis(25));
98            }
99        }
100    }
101    Err(last.unwrap_or_else(|| io::Error::new(io::ErrorKind::TimedOut, "connect retry exhausted")))
102}
103
104struct NetInner {
105    rank: u32,
106    world: u32,
107    heap_size: usize,
108    /// Write half of the connection to each peer (index = peer rank).
109    /// `None` at our own rank and until a peer connects.
110    writers: Vec<Mutex<Option<TcpStream>>>,
111    /// Two-sided inbox: `(src_rank, tag) -> queued payloads`.
112    inbox: Mutex<HashMap<(u32, u32), VecDeque<Vec<u8>>>>,
113    inbox_cv: Condvar,
114    /// This rank's symmetric heap (target of remote PUT / GETREQ).
115    heap: RwLock<Vec<u8>>,
116    /// Responses to our one-sided GETs: `responder_rank -> payloads`.
117    getresp: Mutex<HashMap<u32, VecDeque<Vec<u8>>>>,
118    getresp_cv: Condvar,
119    shutdown: AtomicBool,
120}
121
122impl NetInner {
123    fn send_frame(
124        &self,
125        peer: u32,
126        kind: u8,
127        a: u32,
128        b: u32,
129        payload: &[u8],
130    ) -> Result<(), CollectiveError> {
131        let mut guard = self.writers[peer as usize].lock().unwrap();
132        match guard.as_mut() {
133            Some(s) => {
134                write_frame(s, kind, a, b, payload).map_err(|e| CollectiveError::TransportError {
135                    reason: format!("send to rank {peer}: {e}"),
136                })
137            }
138            None => Err(CollectiveError::TransportError {
139                reason: format!("no connection to rank {peer}"),
140            }),
141        }
142    }
143
144    fn recv_inbox(&self, from: u32, tag: u32) -> Vec<u8> {
145        let mut guard = self.inbox.lock().unwrap();
146        loop {
147            if let Some(q) = guard.get_mut(&(from, tag))
148                && let Some(v) = q.pop_front()
149            {
150                return v;
151            }
152            guard = self.inbox_cv.wait(guard).unwrap();
153        }
154    }
155
156    /// Like [`Self::recv_inbox`] but returns `None` if nothing matching
157    /// `(from, tag)` arrives before `timeout` elapses. Lets a collective
158    /// bound how long it waits on a slow/offline peer (bounded staleness).
159    fn recv_inbox_timeout(&self, from: u32, tag: u32, timeout: Duration) -> Option<Vec<u8>> {
160        let deadline = Instant::now() + timeout;
161        let mut guard = self.inbox.lock().unwrap();
162        loop {
163            if let Some(q) = guard.get_mut(&(from, tag))
164                && let Some(v) = q.pop_front()
165            {
166                return Some(v);
167            }
168            let now = Instant::now();
169            if now >= deadline {
170                return None;
171            }
172            let (g, res) = self.inbox_cv.wait_timeout(guard, deadline - now).unwrap();
173            guard = g;
174            if res.timed_out()
175                && let Some(q) = guard.get_mut(&(from, tag))
176                && let Some(v) = q.pop_front()
177            {
178                return Some(v);
179            }
180        }
181    }
182
183    fn wait_getresp(&self, peer: u32) -> Vec<u8> {
184        let mut guard = self.getresp.lock().unwrap();
185        loop {
186            if let Some(q) = guard.get_mut(&peer)
187                && let Some(v) = q.pop_front()
188            {
189                return v;
190            }
191            guard = self.getresp_cv.wait(guard).unwrap();
192        }
193    }
194
195    fn check_buf(&self, buf: SymmetricBuffer) -> Result<(), CollectiveError> {
196        if buf.rank.0 >= self.world {
197            return Err(CollectiveError::UnknownRank {
198                rank: buf.rank,
199                num_ranks: self.world,
200            });
201        }
202        if buf.offset + buf.len > self.heap_size {
203            return Err(CollectiveError::OutOfBounds {
204                rank: buf.rank,
205                offset: buf.offset,
206                len: buf.len,
207                heap_size: self.heap_size,
208            });
209        }
210        Ok(())
211    }
212
213    fn dispatch(&self, kind: u8, a: u32, b: u32, payload: Vec<u8>) {
214        match kind {
215            SEND => {
216                self.inbox
217                    .lock()
218                    .unwrap()
219                    .entry((a, b))
220                    .or_default()
221                    .push_back(payload);
222                self.inbox_cv.notify_all();
223            }
224            PUT => {
225                let off = b as usize;
226                if off + payload.len() <= self.heap_size {
227                    let mut h = self.heap.write().unwrap();
228                    h[off..off + payload.len()].copy_from_slice(&payload);
229                }
230            }
231            GETREQ if payload.len() >= 8 => {
232                let off = u32::from_le_bytes(payload[0..4].try_into().unwrap()) as usize;
233                let rlen = u32::from_le_bytes(payload[4..8].try_into().unwrap()) as usize;
234                let data = {
235                    let h = self.heap.read().unwrap();
236                    if off + rlen <= self.heap_size {
237                        h[off..off + rlen].to_vec()
238                    } else {
239                        Vec::new()
240                    }
241                };
242                // a = requester rank; respond with our rank as sender.
243                let _ = self.send_frame(a, GETRESP, self.rank, off as u32, &data);
244            }
245            GETRESP => {
246                self.getresp
247                    .lock()
248                    .unwrap()
249                    .entry(a)
250                    .or_default()
251                    .push_back(payload);
252                self.getresp_cv.notify_all();
253            }
254            _ => {}
255        }
256    }
257}
258
259fn reader_loop(mut stream: TcpStream, inner: Arc<NetInner>) {
260    // Exits when read_frame errors (peer closed or we were shut down).
261    while let Ok((kind, a, b, payload)) = read_frame(&mut stream) {
262        inner.dispatch(kind, a, b, payload);
263        if inner.shutdown.load(Ordering::Relaxed) {
264            break;
265        }
266    }
267}
268
269/// Full-mesh TCP transport implementing both [`Transport`] (two-sided
270/// send/recv) and [`SymmetricTransport`] (one-sided put/get/barrier).
271/// Construct via [`TcpTransport::bind`] or [`ThunderboltTransport::bind`].
272pub struct NetTransport {
273    inner: Arc<NetInner>,
274    readers: Mutex<Vec<JoinHandle<()>>>,
275}
276
277impl NetTransport {
278    /// Build a transport from an already-bound listener. Ranks above us
279    /// we connect to; ranks below us we accept from (each connector
280    /// announces itself with a `HELLO`). `peers[r]` is rank `r`'s listen
281    /// address; `peers[rank]` must match `listener`.
282    ///
283    /// Public so callers (and multi-rank tests) can pre-bind ephemeral
284    /// ports, exchange the addresses out-of-band, then construct the
285    /// mesh — avoiding the fixed-port races [`TcpTransport::bind`] is
286    /// subject to.
287    pub fn from_listener(
288        rank: u32,
289        world: u32,
290        listener: TcpListener,
291        peers: Vec<SocketAddr>,
292        heap_size: usize,
293    ) -> io::Result<Self> {
294        assert_eq!(
295            peers.len(),
296            world as usize,
297            "peers must have world_size entries"
298        );
299        assert!(rank < world, "rank out of range");
300
301        let inner = Arc::new(NetInner {
302            rank,
303            world,
304            heap_size,
305            writers: (0..world).map(|_| Mutex::new(None)).collect(),
306            inbox: Mutex::new(HashMap::new()),
307            inbox_cv: Condvar::new(),
308            heap: RwLock::new(vec![0u8; heap_size]),
309            getresp: Mutex::new(HashMap::new()),
310            getresp_cv: Condvar::new(),
311            shutdown: AtomicBool::new(false),
312        });
313        let mut readers = Vec::new();
314
315        // Connect to every higher-ranked peer.
316        for p in (rank + 1)..world {
317            let stream = connect_retry(peers[p as usize])?;
318            stream.set_nodelay(true).ok();
319            let mut wr = stream.try_clone()?;
320            write_frame(&mut wr, HELLO, rank, 0, &[])?;
321            let rd = stream.try_clone()?;
322            let inner2 = inner.clone();
323            readers.push(thread::spawn(move || reader_loop(rd, inner2)));
324            *inner.writers[p as usize].lock().unwrap() = Some(wr);
325        }
326
327        // Accept exactly `rank` connections from lower-ranked peers.
328        for _ in 0..rank {
329            let (stream, _addr) = listener.accept()?;
330            stream.set_nodelay(true).ok();
331            let mut rd = stream.try_clone()?;
332            let (kind, peer, _b, _payload) = read_frame(&mut rd)?;
333            if kind != HELLO || peer >= world {
334                return Err(io::Error::new(
335                    io::ErrorKind::InvalidData,
336                    format!("expected HELLO with valid rank, got kind={kind} rank={peer}"),
337                ));
338            }
339            let wr = stream.try_clone()?;
340            let inner2 = inner.clone();
341            readers.push(thread::spawn(move || reader_loop(rd, inner2)));
342            *inner.writers[peer as usize].lock().unwrap() = Some(wr);
343        }
344
345        Ok(Self {
346            inner,
347            readers: Mutex::new(readers),
348        })
349    }
350}
351
352impl NetTransport {
353    fn empty_inner(rank: u32, world: u32, heap_size: usize) -> Arc<NetInner> {
354        Arc::new(NetInner {
355            rank,
356            world,
357            heap_size,
358            writers: (0..world).map(|_| Mutex::new(None)).collect(),
359            inbox: Mutex::new(HashMap::new()),
360            inbox_cv: Condvar::new(),
361            heap: RwLock::new(vec![0u8; heap_size]),
362            getresp: Mutex::new(HashMap::new()),
363            getresp_cv: Condvar::new(),
364            shutdown: AtomicBool::new(false),
365        })
366    }
367
368    /// Coordinator side of a **star** topology: bind `listener` (rank 0's
369    /// address) and accept `world - 1` worker connections, each announcing its
370    /// rank with a `HELLO`. Workers connect only to the coordinator (no
371    /// worker↔worker links), so a worker needs **no inbound port** — the
372    /// NAT/mobile-friendly shape for iOS, phones, and Pis behind a router.
373    pub fn coordinator_listen(
374        world: u32,
375        listener: TcpListener,
376        heap_size: usize,
377    ) -> io::Result<Self> {
378        let inner = Self::empty_inner(0, world, heap_size);
379        let mut readers = Vec::new();
380        for _ in 1..world {
381            let (stream, _addr) = listener.accept()?;
382            stream.set_nodelay(true).ok();
383            let mut rd = stream.try_clone()?;
384            let (kind, peer, _b, _payload) = read_frame(&mut rd)?;
385            if kind != HELLO || peer == 0 || peer >= world {
386                return Err(io::Error::new(
387                    io::ErrorKind::InvalidData,
388                    format!("expected HELLO from a worker rank, got kind={kind} rank={peer}"),
389                ));
390            }
391            let wr = stream.try_clone()?;
392            let inner2 = inner.clone();
393            readers.push(thread::spawn(move || reader_loop(rd, inner2)));
394            *inner.writers[peer as usize].lock().unwrap() = Some(wr);
395        }
396        Ok(Self {
397            inner,
398            readers: Mutex::new(readers),
399        })
400    }
401
402    /// Worker side of a **star** topology: dial the coordinator at `coord_addr`
403    /// and announce this `rank`. No listener — the worker only needs outbound
404    /// connectivity, so it works from behind NAT / on mobile.
405    pub fn worker_dial(
406        rank: u32,
407        world: u32,
408        coord_addr: SocketAddr,
409        heap_size: usize,
410    ) -> io::Result<Self> {
411        assert!(rank > 0 && rank < world, "worker rank must be in 1..world");
412        let inner = Self::empty_inner(rank, world, heap_size);
413        let stream = connect_retry(coord_addr)?;
414        stream.set_nodelay(true).ok();
415        let mut wr = stream.try_clone()?;
416        write_frame(&mut wr, HELLO, rank, 0, &[])?;
417        let rd = stream.try_clone()?;
418        let inner2 = inner.clone();
419        let readers = vec![thread::spawn(move || reader_loop(rd, inner2))];
420        *inner.writers[0].lock().unwrap() = Some(wr);
421        Ok(Self {
422            inner,
423            readers: Mutex::new(readers),
424        })
425    }
426}
427
428impl Drop for NetTransport {
429    fn drop(&mut self) {
430        self.inner.shutdown.store(true, Ordering::SeqCst);
431        // Closing each socket makes the reader threads' read_exact fail.
432        for w in &self.inner.writers {
433            if let Some(s) = w.lock().unwrap().as_ref() {
434                let _ = s.shutdown(Shutdown::Both);
435            }
436        }
437        self.inner.inbox_cv.notify_all();
438        self.inner.getresp_cv.notify_all();
439        if let Ok(mut hs) = self.readers.lock() {
440            for h in hs.drain(..) {
441                let _ = h.join();
442            }
443        }
444    }
445}
446
447impl Transport for NetTransport {
448    fn rank(&self) -> u32 {
449        self.inner.rank
450    }
451    fn world_size(&self) -> u32 {
452        self.inner.world
453    }
454    fn send_bytes(&self, to: u32, tag: u32, bytes: &[u8]) -> Result<(), CollectiveError> {
455        if to == self.inner.rank {
456            // Self-message: deliver straight to our own inbox.
457            self.inner
458                .inbox
459                .lock()
460                .unwrap()
461                .entry((to, tag))
462                .or_default()
463                .push_back(bytes.to_vec());
464            self.inner.inbox_cv.notify_all();
465            return Ok(());
466        }
467        self.inner.send_frame(to, SEND, self.inner.rank, tag, bytes)
468    }
469    fn recv_bytes(&self, from: u32, tag: u32) -> Result<Vec<u8>, CollectiveError> {
470        Ok(self.inner.recv_inbox(from, tag))
471    }
472    fn recv_bytes_timeout(
473        &self,
474        from: u32,
475        tag: u32,
476        timeout: Duration,
477    ) -> Result<Option<Vec<u8>>, CollectiveError> {
478        Ok(self.inner.recv_inbox_timeout(from, tag, timeout))
479    }
480    fn barrier(&self) -> Result<(), CollectiveError> {
481        default_barrier(self)
482    }
483}
484
485impl SymmetricTransport for NetTransport {
486    fn num_ranks(&self) -> u32 {
487        self.inner.world
488    }
489    fn this_rank(&self) -> Rank {
490        Rank(self.inner.rank)
491    }
492    fn put(&self, buf: SymmetricBuffer, src: &[u8]) -> Result<(), CollectiveError> {
493        self.inner.check_buf(buf)?;
494        if src.len() != buf.len {
495            return Err(CollectiveError::LengthMismatch {
496                expected: buf.len,
497                got: src.len(),
498            });
499        }
500        if buf.rank.0 == self.inner.rank {
501            let mut h = self.inner.heap.write().unwrap();
502            h[buf.offset..buf.offset + buf.len].copy_from_slice(src);
503            Ok(())
504        } else {
505            self.inner
506                .send_frame(buf.rank.0, PUT, self.inner.rank, buf.offset as u32, src)
507        }
508    }
509    fn get(&self, buf: SymmetricBuffer, dst: &mut [u8]) -> Result<(), CollectiveError> {
510        self.inner.check_buf(buf)?;
511        if dst.len() != buf.len {
512            return Err(CollectiveError::LengthMismatch {
513                expected: buf.len,
514                got: dst.len(),
515            });
516        }
517        if buf.rank.0 == self.inner.rank {
518            let h = self.inner.heap.read().unwrap();
519            dst.copy_from_slice(&h[buf.offset..buf.offset + buf.len]);
520            Ok(())
521        } else {
522            let mut payload = [0u8; 8];
523            payload[0..4].copy_from_slice(&(buf.offset as u32).to_le_bytes());
524            payload[4..8].copy_from_slice(&(buf.len as u32).to_le_bytes());
525            self.inner
526                .send_frame(buf.rank.0, GETREQ, self.inner.rank, 0, &payload)?;
527            let data = self.inner.wait_getresp(buf.rank.0);
528            if data.len() != buf.len {
529                return Err(CollectiveError::LengthMismatch {
530                    expected: buf.len,
531                    got: data.len(),
532                });
533            }
534            dst.copy_from_slice(&data);
535            Ok(())
536        }
537    }
538    fn barrier(&self) -> Result<(), CollectiveError> {
539        default_barrier(self)
540    }
541}
542
543/// Default symmetric-heap size per rank (bytes) when a constructor is
544/// not given one. 64 MiB comfortably holds a single transformer hidden
545/// state at typical batch×seq×d_model for collective scratch.
546pub const DEFAULT_HEAP_BYTES: usize = 64 * 1024 * 1024;
547
548/// Portable TCP transport. Works over any reachable IP — Ethernet or
549/// the macOS Thunderbolt Bridge link. This is the baseline that
550/// pipeline- and tensor-parallel inference run on before any fast path.
551pub struct TcpTransport;
552
553impl TcpTransport {
554    /// Bind this rank's listener at `peers[rank]` and establish the full
555    /// mesh. Every rank must pass the same `peers` list.
556    pub fn bind(
557        rank: u32,
558        world: u32,
559        peers: Vec<SocketAddr>,
560        heap_size: usize,
561    ) -> io::Result<NetTransport> {
562        let listener = TcpListener::bind(peers[rank as usize])?;
563        NetTransport::from_listener(rank, world, listener, peers, heap_size)
564    }
565}
566
567/// Thunderbolt transport. Same wire protocol as [`TcpTransport`] today
568/// (TCP over the Thunderbolt Bridge IP link), but a distinct type so
569/// the symmetric one-sided heap path is the intended interface and so a
570/// future zero-copy Thunderbolt DMA backend can replace the engine
571/// behind the unchanged [`Transport`] + [`SymmetricTransport`] traits.
572pub struct ThunderboltTransport;
573
574impl ThunderboltTransport {
575    /// Bind at `peers[rank]`. The addresses are expected to be on the
576    /// Thunderbolt interface (see [`ThunderboltTransport::looks_like_thunderbolt`]);
577    /// correctness does not depend on it, but bandwidth does.
578    pub fn bind(
579        rank: u32,
580        world: u32,
581        peers: Vec<SocketAddr>,
582        heap_size: usize,
583    ) -> io::Result<NetTransport> {
584        TcpTransport::bind(rank, world, peers, heap_size)
585    }
586
587    /// Heuristic: is `ip` plausibly a Thunderbolt Bridge address? macOS
588    /// auto-assigns link-local `169.254.0.0/16` to the bridge when no
589    /// static IP is configured; static setups commonly use a private
590    /// `10.0.0.0/8` block. This is advisory only.
591    pub fn looks_like_thunderbolt(ip: IpAddr) -> bool {
592        match ip {
593            IpAddr::V4(v4) => {
594                let o = v4.octets();
595                o[0] == 169 && o[1] == 254 || o[0] == 10
596            }
597            IpAddr::V6(_) => false,
598        }
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605    use crate::collective::{ReduceKind, all_reduce};
606    use crate::transport::ProcessGroup;
607
608    /// Pre-bind `n` loopback listeners and run `body(rank, transport)`
609    /// on a thread per rank. Returns once every rank's body returns.
610    fn run_net<F>(n: u32, heap: usize, body: F)
611    where
612        F: Fn(u32, Arc<NetTransport>) + Send + Sync + 'static,
613    {
614        let listeners: Vec<TcpListener> = (0..n)
615            .map(|_| TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).unwrap())
616            .collect();
617        let addrs: Vec<SocketAddr> = listeners.iter().map(|l| l.local_addr().unwrap()).collect();
618        let body = Arc::new(body);
619        let handles: Vec<_> = listeners
620            .into_iter()
621            .enumerate()
622            .map(|(rank, listener)| {
623                let addrs = addrs.clone();
624                let body = body.clone();
625                thread::spawn(move || {
626                    let t = Arc::new(
627                        NetTransport::from_listener(rank as u32, n, listener, addrs, heap)
628                            .expect("transport build"),
629                    );
630                    body(rank as u32, t.clone());
631                    // Trailing barrier: keep every rank's reader threads alive
632                    // until all peers finish, so a late remote GET still gets a
633                    // response before any transport is dropped.
634                    <NetTransport as Transport>::barrier(&*t).unwrap();
635                })
636            })
637            .collect();
638        for h in handles {
639            h.join().unwrap();
640        }
641    }
642
643    /// Star variant of [`run_net`]: rank 0 listens (coordinator) and ranks
644    /// 1..n dial in (workers, no inbound port of their own).
645    fn run_star<F>(n: u32, heap: usize, body: F)
646    where
647        F: Fn(u32, Arc<NetTransport>) + Send + Sync + 'static,
648    {
649        let listener = TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).unwrap();
650        let coord_addr = listener.local_addr().unwrap();
651        let body = Arc::new(body);
652        let mut handles = Vec::new();
653        {
654            let body = body.clone();
655            handles.push(thread::spawn(move || {
656                let t = Arc::new(NetTransport::coordinator_listen(n, listener, heap).unwrap());
657                body(0, t.clone());
658                <NetTransport as Transport>::barrier(&*t).unwrap();
659            }));
660        }
661        for rank in 1..n {
662            let body = body.clone();
663            handles.push(thread::spawn(move || {
664                let t = Arc::new(NetTransport::worker_dial(rank, n, coord_addr, heap).unwrap());
665                body(rank, t.clone());
666                <NetTransport as Transport>::barrier(&*t).unwrap();
667            }));
668        }
669        for h in handles {
670            h.join().unwrap();
671        }
672    }
673
674    #[test]
675    fn star_dial_out_coordinator_worker() {
676        // Workers dial the coordinator (no listener of their own); the
677        // coordinator sums their contributions and sends the result back.
678        run_star(3, 4096, |rank, t| {
679            let g = ProcessGroup::new(t);
680            if rank == 0 {
681                let a = g.recv_f32(1, 5).unwrap();
682                let b = g.recv_f32(2, 5).unwrap();
683                let sum: Vec<f32> = a.iter().zip(&b).map(|(x, y)| x + y).collect();
684                g.send_f32(1, 6, &sum).unwrap();
685                g.send_f32(2, 6, &sum).unwrap();
686            } else {
687                g.send_f32(0, 5, &[rank as f32]).unwrap();
688                assert_eq!(g.recv_f32(0, 6).unwrap(), vec![3.0]); // 1 + 2
689            }
690        });
691    }
692
693    #[test]
694    fn two_sided_pipeline_handoff() {
695        // rank r sends [r*10..] to r-1 (toward the leader), like a
696        // pipeline returning hidden states.
697        run_net(3, 4096, |rank, t| {
698            let g = ProcessGroup::new(t);
699            let n = g.world_size();
700            if rank + 1 < n {
701                let got = g.recv_f32(rank + 1, 7).unwrap();
702                assert_eq!(got, vec![(rank as f32 + 1.0) * 10.0, 1.0]);
703            }
704            if rank > 0 {
705                g.send_f32(rank - 1, 7, &[rank as f32 * 10.0, 1.0]).unwrap();
706            }
707        });
708    }
709
710    #[test]
711    fn process_group_all_reduce_over_tcp() {
712        run_net(4, 4096, |rank, t| {
713            let g = ProcessGroup::new(t);
714            let mut data = vec![rank as f32 + 1.0; 5];
715            g.all_reduce(&mut data, ReduceKind::Sum).unwrap();
716            assert_eq!(data, vec![10.0; 5], "rank {rank}");
717        });
718    }
719
720    #[test]
721    fn federated_average_skips_late_worker() {
722        // rank 2 behaves like an offline/slow edge client — it only sends
723        // after the deadline, so rank 0 drops it and averages ranks 0 and 1.
724        run_net(3, 4096, |rank, t| {
725            let g = ProcessGroup::new(t);
726            let mut data = vec![rank as f32 + 1.0; 3]; // 1, 2, 3
727            if rank == 2 {
728                std::thread::sleep(Duration::from_millis(300));
729            }
730            let present = g
731                .federated_average(&mut data, Duration::from_millis(100))
732                .unwrap();
733            if rank == 0 {
734                assert_eq!(present, 2, "late rank 2 must be dropped");
735                assert_eq!(data, vec![1.5; 3]); // mean of ranks 0 and 1
736            }
737        });
738    }
739
740    #[test]
741    fn process_group_barrier_and_broadcast() {
742        run_net(3, 4096, |rank, t| {
743            let g = ProcessGroup::new(t);
744            g.barrier().unwrap();
745            let mut data = if rank == 0 {
746                vec![1.0, 2.0]
747            } else {
748                vec![0.0, 0.0]
749            };
750            g.broadcast(0, &mut data).unwrap();
751            assert_eq!(data, vec![1.0, 2.0], "rank {rank}");
752        });
753    }
754
755    #[test]
756    fn symmetric_remote_put_get() {
757        // Rank 0 PUTs into rank 1's heap; rank 1 reads its own slot and
758        // sees it. Then rank 1 GETs from rank 0's heap.
759        run_net(2, 4096, |rank, t| {
760            let off = 128;
761            let payload = [5u8, 6, 7, 8];
762            if rank == 0 {
763                // seed our own heap for rank 1 to GET later
764                t.put(
765                    SymmetricBuffer {
766                        rank: Rank(0),
767                        offset: off,
768                        len: 4,
769                    },
770                    &[1u8, 2, 3, 4],
771                )
772                .unwrap();
773                // write into rank 1's heap
774                t.put(
775                    SymmetricBuffer {
776                        rank: Rank(1),
777                        offset: off,
778                        len: 4,
779                    },
780                    &payload,
781                )
782                .unwrap();
783            }
784            // Make sure the PUT landed before the reads.
785            <NetTransport as Transport>::barrier(&t).unwrap();
786            // ^ disambiguates Transport::barrier from SymmetricTransport::barrier
787            if rank == 1 {
788                let mut got = [0u8; 4];
789                t.get(
790                    SymmetricBuffer {
791                        rank: Rank(1),
792                        offset: off,
793                        len: 4,
794                    },
795                    &mut got,
796                )
797                .unwrap();
798                assert_eq!(got, payload, "local read of remote PUT");
799                let mut remote = [0u8; 4];
800                t.get(
801                    SymmetricBuffer {
802                        rank: Rank(0),
803                        offset: off,
804                        len: 4,
805                    },
806                    &mut remote,
807                )
808                .unwrap();
809                assert_eq!(remote, [1u8, 2, 3, 4], "remote GET from rank 0");
810            }
811        });
812    }
813
814    #[test]
815    fn symmetric_collective_all_reduce_over_net() {
816        // The existing one-sided collective (collective::all_reduce)
817        // runs unmodified over NetTransport.
818        run_net(4, 4096, |rank, t| {
819            let elems = 3usize;
820            let bytes = elems * 4;
821            let buf = SymmetricBuffer {
822                rank: Rank(rank),
823                offset: 0,
824                len: bytes,
825            };
826            let mut local = vec![rank as f32 + 1.0; elems];
827            all_reduce(&*t, buf, &mut local, ReduceKind::Sum).unwrap();
828            assert_eq!(local, vec![10.0; elems], "rank {rank}");
829        });
830    }
831
832    #[test]
833    fn thunderbolt_addr_heuristic() {
834        use std::net::Ipv4Addr;
835        assert!(ThunderboltTransport::looks_like_thunderbolt(
836            Ipv4Addr::new(169, 254, 3, 1).into()
837        ));
838        assert!(ThunderboltTransport::looks_like_thunderbolt(
839            Ipv4Addr::new(10, 0, 0, 2).into()
840        ));
841        assert!(!ThunderboltTransport::looks_like_thunderbolt(
842            Ipv4Addr::new(192, 168, 1, 5).into()
843        ));
844    }
845}