Skip to main content

ts_collective/
lib.rs

1// Copyright (c) 2026 LightSeek Foundation
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! CPU collectives among the ranks of an engine: what the control plane's
22//! gloo groups do today, without the GIL.
23//!
24//! Every pair of ranks shares one TCP connection. A collective blocks until
25//! it completes, and every member of a group must run the same collectives
26//! on it in the same order: the rank-identical ordering the control plane
27//! keeps anyway (`docs/design/event-loop.md`). A reader thread per
28//! connection drains the socket into a queue, so a send never waits on the
29//! peer reading; a collective sends this rank's part to the members that
30//! need it, then takes each part it needs from its sender's queue.
31//!
32//! Each frame names its collective's kind, group and sequence number on that
33//! group, so a member that took another path fails loudly instead of reading
34//! another collective's bytes. A peer that exits fails every collective
35//! waiting on it.
36
37use std::{
38    collections::HashMap,
39    io::{self, Read, Write},
40    net::{Shutdown, TcpListener, TcpStream, ToSocketAddrs},
41    sync::mpsc,
42    thread,
43    time::{Duration, Instant},
44};
45
46/// What a mesh connection says first: who is calling.
47const HELLO_MAGIC: u32 = 0x5453_434d; // "TSCM"
48
49/// A collective failure; the ranks can no longer agree.
50#[derive(Debug, thiserror::Error)]
51pub enum Error {
52    /// A socket operation failed.
53    #[error("collective I/O: {0}")]
54    Io(#[from] io::Error),
55    /// The peers did not all connect in time.
56    #[error("the mesh did not connect within {0:?}")]
57    Timeout(Duration),
58    /// A connection did not introduce itself as a rank of this mesh.
59    #[error("bad mesh handshake: {0}")]
60    Handshake(String),
61    /// The ranks do not form a valid group.
62    #[error("invalid group: {0}")]
63    InvalidGroup(String),
64    /// A peer's connection closed.
65    #[error("rank {0} left the mesh")]
66    PeerClosed(usize),
67    /// A peer sent a frame of another collective.
68    #[error("rank {rank} is out of step: expected {expected:?}, received {received:?}")]
69    Desync {
70        /// The peer.
71        rank: usize,
72        /// The collective this rank is in.
73        expected: Tag,
74        /// The collective the peer sent.
75        received: Tag,
76    },
77    /// The members passed reductions of different lengths.
78    #[error("rank {rank} reduced {found} values, this rank {expected}")]
79    LengthMismatch {
80        /// The peer.
81        rank: usize,
82        /// Values this rank passed.
83        expected: usize,
84        /// Values the peer passed.
85        found: usize,
86    },
87}
88
89/// Which collective a frame belongs to.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct Tag {
92    /// The collective's kind.
93    pub kind: u8,
94    /// Its group.
95    pub group: u32,
96    /// Its position among the group's collectives.
97    pub sequence: u64,
98}
99
100const BROADCAST: u8 = 1;
101const ALL_GATHER: u8 = 2;
102
103/// An element-wise reduction.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Reduce {
106    /// The largest value.
107    Max,
108    /// The smallest value.
109    Min,
110    /// The sum.
111    Sum,
112}
113
114/// A set of ranks that run collectives together, as one rank sees it.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct Group {
117    id: u32,
118    ranks: Vec<usize>,
119    index: usize,
120}
121
122impl Group {
123    /// The group `id` of `ranks` (global ranks, in member order), from
124    /// `rank`'s side. Every member must use the same id for it, and no two
125    /// groups the same id.
126    pub fn new(id: u32, ranks: Vec<usize>, rank: usize) -> Result<Self, Error> {
127        let mut sorted = ranks.clone();
128        sorted.sort_unstable();
129        sorted.dedup();
130        if sorted.len() != ranks.len() {
131            return Err(Error::InvalidGroup(format!("{ranks:?} repeats a rank")));
132        }
133        let index = ranks
134            .iter()
135            .position(|member| *member == rank)
136            .ok_or_else(|| Error::InvalidGroup(format!("rank {rank} is not in {ranks:?}")))?;
137        Ok(Self { id, ranks, index })
138    }
139
140    /// The group's members, as global ranks.
141    pub fn ranks(&self) -> &[usize] {
142        &self.ranks
143    }
144
145    /// Members.
146    pub fn size(&self) -> usize {
147        self.ranks.len()
148    }
149
150    /// This rank's position among the members.
151    pub fn index(&self) -> usize {
152        self.index
153    }
154}
155
156/// This rank's listening socket, bound before the ranks exchange addresses.
157pub struct Listener(TcpListener);
158
159impl Listener {
160    /// Bind an ephemeral port on `host`.
161    pub fn bind(host: &str) -> io::Result<Self> {
162        TcpListener::bind((host, 0)).map(Self)
163    }
164
165    /// The bound port, for the address the other ranks dial.
166    pub fn port(&self) -> io::Result<u16> {
167        Ok(self.0.local_addr()?.port())
168    }
169}
170
171type Frame = (Tag, Vec<u8>);
172
173struct Peer {
174    stream: TcpStream,
175    frames: mpsc::Receiver<io::Result<Frame>>,
176    reader: Option<thread::JoinHandle<()>>,
177}
178
179/// One rank's connections to every other rank.
180pub struct Mesh {
181    rank: usize,
182    peers: Vec<Option<Peer>>,
183    sequences: HashMap<u32, u64>,
184}
185
186fn write_u32(out: &mut Vec<u8>, value: u32) {
187    out.extend_from_slice(&value.to_le_bytes());
188}
189
190fn read_u32(stream: &mut impl Read) -> io::Result<u32> {
191    let mut bytes = [0; 4];
192    stream.read_exact(&mut bytes)?;
193    Ok(u32::from_le_bytes(bytes))
194}
195
196fn read_u64(stream: &mut impl Read) -> io::Result<u64> {
197    let mut bytes = [0; 8];
198    stream.read_exact(&mut bytes)?;
199    Ok(u64::from_le_bytes(bytes))
200}
201
202fn read_frame(stream: &mut impl Read) -> io::Result<Frame> {
203    let mut kind = [0; 1];
204    stream.read_exact(&mut kind)?;
205    let group = read_u32(stream)?;
206    let sequence = read_u64(stream)?;
207    let len = usize::try_from(read_u64(stream)?)
208        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "frame too long"))?;
209    let mut payload = vec![0; len];
210    stream.read_exact(&mut payload)?;
211    let tag = Tag {
212        kind: kind[0],
213        group,
214        sequence,
215    };
216    Ok((tag, payload))
217}
218
219fn frame_header(tag: Tag, len: usize) -> Vec<u8> {
220    let mut header = Vec::with_capacity(21);
221    header.push(tag.kind);
222    write_u32(&mut header, tag.group);
223    header.extend_from_slice(&tag.sequence.to_le_bytes());
224    header.extend_from_slice(&(len as u64).to_le_bytes());
225    header
226}
227
228fn hello(rank: usize, world: usize) -> Vec<u8> {
229    let mut out = Vec::with_capacity(12);
230    write_u32(&mut out, HELLO_MAGIC);
231    write_u32(&mut out, rank as u32);
232    write_u32(&mut out, world as u32);
233    out
234}
235
236fn remaining(deadline: Instant, timeout: Duration) -> Result<Duration, Error> {
237    deadline
238        .checked_duration_since(Instant::now())
239        .filter(|left| !left.is_zero())
240        .ok_or(Error::Timeout(timeout))
241}
242
243impl Mesh {
244    /// Connect `rank` to every other rank of `addresses` (`host:port`, one
245    /// per rank, in rank order). Every rank calls this; it returns once
246    /// every connection is up, so it is also a barrier.
247    pub fn connect(
248        rank: usize,
249        listener: Listener,
250        addresses: &[String],
251        timeout: Duration,
252    ) -> Result<Self, Error> {
253        let world = addresses.len();
254        if rank >= world {
255            return Err(Error::InvalidGroup(format!(
256                "rank {rank} of a {world}-rank mesh"
257            )));
258        }
259        let deadline = Instant::now() + timeout;
260        let mut streams: Vec<Option<TcpStream>> = (0..world).map(|_| None).collect();
261
262        // Each rank dials the lower ranks and accepts the higher ones.
263        for (peer, address) in addresses.iter().enumerate().take(rank) {
264            let stream = Self::dial(address, deadline, timeout)?;
265            (&stream).write_all(&hello(rank, world))?;
266            streams[peer] = Some(stream);
267        }
268        let listener = listener.0;
269        listener.set_nonblocking(true)?;
270        let mut expected = world - rank - 1;
271        while expected > 0 {
272            match listener.accept() {
273                Ok((stream, _)) => {
274                    stream.set_nonblocking(false)?;
275                    stream.set_read_timeout(Some(remaining(deadline, timeout)?))?;
276                    let mut reader = &stream;
277                    let (magic, peer, peer_world) = (
278                        read_u32(&mut reader)?,
279                        read_u32(&mut reader)? as usize,
280                        read_u32(&mut reader)? as usize,
281                    );
282                    if magic != HELLO_MAGIC || peer_world != world || peer <= rank || peer >= world
283                    {
284                        return Err(Error::Handshake(format!(
285                            "magic {magic:#x} rank {peer} of {peer_world} at rank {rank} of {world}"
286                        )));
287                    }
288                    if streams[peer].is_some() {
289                        return Err(Error::Handshake(format!("rank {peer} connected twice")));
290                    }
291                    stream.set_read_timeout(None)?;
292                    streams[peer] = Some(stream);
293                    expected -= 1;
294                }
295                Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
296                    remaining(deadline, timeout)?;
297                    thread::sleep(Duration::from_millis(5));
298                }
299                Err(error) => return Err(error.into()),
300            }
301        }
302
303        let mut peers = Vec::with_capacity(world);
304        for (peer, stream) in streams.into_iter().enumerate() {
305            let Some(stream) = stream else {
306                peers.push(None);
307                continue;
308            };
309            stream.set_nodelay(true)?;
310            let mut reading = stream.try_clone()?;
311            let (sender, frames) = mpsc::channel();
312            let reader = thread::Builder::new()
313                .name(format!("ts-mesh-{peer}"))
314                .spawn(move || loop {
315                    let frame = read_frame(&mut reading);
316                    let failed = frame.is_err();
317                    if sender.send(frame).is_err() || failed {
318                        return;
319                    }
320                })?;
321            peers.push(Some(Peer {
322                stream,
323                frames,
324                reader: Some(reader),
325            }));
326        }
327        tracing::info!(rank, world, "collective mesh connected");
328        Ok(Self {
329            rank,
330            peers,
331            sequences: HashMap::new(),
332        })
333    }
334
335    fn dial(address: &str, deadline: Instant, timeout: Duration) -> Result<TcpStream, Error> {
336        let resolved: Vec<_> = address.to_socket_addrs()?.collect();
337        let target = *resolved
338            .first()
339            .ok_or_else(|| Error::Handshake(format!("{address} resolves to nothing")))?;
340        loop {
341            match TcpStream::connect_timeout(&target, remaining(deadline, timeout)?) {
342                Ok(stream) => return Ok(stream),
343                // The peer bound its listener before publishing the address,
344                // so a refusal is transient only while it starts up.
345                Err(error) if error.kind() == io::ErrorKind::ConnectionRefused => {
346                    thread::sleep(Duration::from_millis(20));
347                }
348                Err(error) => return Err(error.into()),
349            }
350        }
351    }
352
353    /// This rank.
354    pub fn rank(&self) -> usize {
355        self.rank
356    }
357
358    fn next_tag(&mut self, kind: u8, group: &Group) -> Tag {
359        let sequence = self.sequences.entry(group.id).or_insert(0);
360        *sequence += 1;
361        Tag {
362            kind,
363            group: group.id,
364            sequence: *sequence,
365        }
366    }
367
368    fn peer(&mut self, rank: usize) -> &mut Peer {
369        self.peers[rank]
370            .as_mut()
371            .expect("a member other than this rank")
372    }
373
374    fn send(&mut self, rank: usize, tag: Tag, payload: &[u8]) -> Result<(), Error> {
375        let header = frame_header(tag, payload.len());
376        let stream = &mut self.peer(rank).stream;
377        stream
378            .write_all(&header)
379            .and_then(|()| stream.write_all(payload))
380            .map_err(|error| match error.kind() {
381                io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionReset => {
382                    Error::PeerClosed(rank)
383                }
384                _ => Error::Io(error),
385            })
386    }
387
388    fn receive(&mut self, rank: usize, expected: Tag) -> Result<Vec<u8>, Error> {
389        let frame = self
390            .peer(rank)
391            .frames
392            .recv()
393            .map_err(|_| Error::PeerClosed(rank))?;
394        let (received, payload) = frame.map_err(|error| match error.kind() {
395            io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset => {
396                Error::PeerClosed(rank)
397            }
398            _ => Error::Io(error),
399        })?;
400        if received != expected {
401            return Err(Error::Desync {
402                rank,
403                expected,
404                received,
405            });
406        }
407        Ok(payload)
408    }
409
410    /// The payload of the member at `root` (an index into the group), on
411    /// every member. Members other than the root pass any payload; it is
412    /// ignored.
413    pub fn broadcast(
414        &mut self,
415        group: &Group,
416        root: usize,
417        payload: Vec<u8>,
418    ) -> Result<Vec<u8>, Error> {
419        if group.size() == 1 {
420            return Ok(payload);
421        }
422        let tag = self.next_tag(BROADCAST, group);
423        if group.index() == root {
424            for (index, &member) in group.ranks().iter().enumerate() {
425                if index != root {
426                    self.send(member, tag, &payload)?;
427                }
428            }
429            Ok(payload)
430        } else {
431            self.receive(group.ranks()[root], tag)
432        }
433    }
434
435    /// Every member's payload, in member order.
436    pub fn all_gather(&mut self, group: &Group, payload: &[u8]) -> Result<Vec<Vec<u8>>, Error> {
437        if group.size() == 1 {
438            return Ok(vec![payload.to_vec()]);
439        }
440        let tag = self.next_tag(ALL_GATHER, group);
441        for (index, &member) in group.ranks().iter().enumerate() {
442            if index != group.index() {
443                self.send(member, tag, payload)?;
444            }
445        }
446        let mut gathered = Vec::with_capacity(group.size());
447        for (index, &member) in group.ranks().iter().enumerate() {
448            if index == group.index() {
449                gathered.push(payload.to_vec());
450            } else {
451                gathered.push(self.receive(member, tag)?);
452            }
453        }
454        Ok(gathered)
455    }
456
457    /// `values` reduced element-wise across the members.
458    pub fn all_reduce(
459        &mut self,
460        group: &Group,
461        values: &[i64],
462        op: Reduce,
463    ) -> Result<Vec<i64>, Error> {
464        let mut rows = self.all_gather_i64(group, values)?.into_iter();
465        let mut reduced = rows.next().expect("a group has a member");
466        for row in rows {
467            for (out, value) in reduced.iter_mut().zip(row) {
468                *out = match op {
469                    Reduce::Max => (*out).max(value),
470                    Reduce::Min => (*out).min(value),
471                    Reduce::Sum => *out + value,
472                };
473            }
474        }
475        Ok(reduced)
476    }
477
478    /// Every member's `values`, in member order; all of the same length.
479    pub fn all_gather_i64(
480        &mut self,
481        group: &Group,
482        values: &[i64],
483    ) -> Result<Vec<Vec<i64>>, Error> {
484        let bytes: Vec<u8> = values
485            .iter()
486            .flat_map(|value| value.to_le_bytes())
487            .collect();
488        let gathered = self.all_gather(group, &bytes)?;
489        let mut rows = Vec::with_capacity(gathered.len());
490        for (index, row) in gathered.into_iter().enumerate() {
491            if row.len() != bytes.len() {
492                return Err(Error::LengthMismatch {
493                    rank: group.ranks()[index],
494                    expected: values.len(),
495                    found: row.len() / 8,
496                });
497            }
498            rows.push(
499                row.chunks_exact(8)
500                    .map(|chunk| i64::from_le_bytes(chunk.try_into().expect("eight bytes")))
501                    .collect(),
502            );
503        }
504        Ok(rows)
505    }
506}
507
508impl Drop for Mesh {
509    fn drop(&mut self) {
510        for peer in self.peers.iter_mut().flatten() {
511            let _ = peer.stream.shutdown(Shutdown::Both);
512            if let Some(reader) = peer.reader.take() {
513                let _ = reader.join();
514            }
515        }
516    }
517}