Skip to main content

rlx_driver/
symmetric.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//! Symmetric-memory primitives for collective ops (plan #49).
6//!
7//! Borrowed from MAX's `kernels/src/shmem/{_nvshmem, _rocshmem,
8//! _mpi, shmem_buffer, ep_comm}.mojo`. Symmetric heaps are the
9//! standard abstraction for multi-device collective comm: every
10//! rank allocates the same logical region; addresses are
11//! identical across ranks (the physical backing differs). One
12//! rank can `put` directly into another's slot at the same
13//! offset.
14//!
15//! The Rust spelling here ships:
16//!
17//!   - [`SymmetricHeap`] — owns the per-rank physical storage.
18//!     Single-machine emulation today (one `Vec<u8>` per rank);
19//!     the trait surface ([`SymmetricTransport`]) is what a
20//!     future MPI / NVSHMEM-equivalent / process-shared-memory
21//!     impl plugs into.
22//!   - [`SymmetricBuffer`] — a `(rank, offset, len)` view.
23//!   - [`SymmetricTransport`] — the trait every transport
24//!     impl satisfies. `LocalTransport` is the in-process,
25//!     single-machine impl used by tests + collective-algo
26//!     correctness checks (plan #12).
27//!
28//! This is the foundation; #12 builds AllReduce / AllGather /
29//! ReduceScatter on top.
30
31use std::sync::{Arc, RwLock};
32
33/// Identifier for a participant in a collective. Ranks are
34/// `0..num_ranks` and stay stable for the lifetime of a
35/// transport.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
37pub struct Rank(pub u32);
38
39/// `(rank, offset, len)` view into a symmetric heap. The same
40/// `(offset, len)` pair is valid on every rank — that's what
41/// "symmetric" means.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct SymmetricBuffer {
44    pub rank: Rank,
45    pub offset: usize,
46    pub len: usize,
47}
48
49/// One-sided operation surface. `put(buf, src)` writes `src`
50/// into `buf.rank`'s memory at `buf.offset`; `get(buf, dst)`
51/// reads from `buf.rank`'s memory into `dst`. Both calls block
52/// until completion (a future async impl can return a future).
53pub trait SymmetricTransport {
54    /// How many ranks participate.
55    fn num_ranks(&self) -> u32;
56    /// This process's rank.
57    fn this_rank(&self) -> Rank;
58
59    /// Write `src` into `buf`. Errors on length mismatch.
60    fn put(&self, buf: SymmetricBuffer, src: &[u8]) -> Result<(), CollectiveError>;
61    /// Read from `buf` into `dst`. Errors on length mismatch.
62    fn get(&self, buf: SymmetricBuffer, dst: &mut [u8]) -> Result<(), CollectiveError>;
63
64    /// Block until every rank has reached this barrier. Local
65    /// emulation is a memory fence + a counter; real transports
66    /// implement their own.
67    fn barrier(&self) -> Result<(), CollectiveError>;
68}
69
70#[derive(Debug, Clone)]
71pub enum CollectiveError {
72    /// `(rank, offset, len)` walks past the heap.
73    OutOfBounds {
74        rank: Rank,
75        offset: usize,
76        len: usize,
77        heap_size: usize,
78    },
79    /// `src.len() != buf.len`.
80    LengthMismatch { expected: usize, got: usize },
81    /// Unknown rank id.
82    UnknownRank { rank: Rank, num_ranks: u32 },
83    /// Underlying transport failed (network, mmap, etc.).
84    TransportError { reason: String },
85}
86
87impl std::fmt::Display for CollectiveError {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            Self::OutOfBounds {
91                rank,
92                offset,
93                len,
94                heap_size,
95            } => write!(
96                f,
97                "OOB on rank {}: offset {offset} + len {len} > heap_size {heap_size}",
98                rank.0
99            ),
100            Self::LengthMismatch { expected, got } => {
101                write!(f, "length mismatch: expected {expected}, got {got}")
102            }
103            Self::UnknownRank { rank, num_ranks } => {
104                write!(f, "unknown rank {} (have {num_ranks})", rank.0)
105            }
106            Self::TransportError { reason } => write!(f, "transport: {reason}"),
107        }
108    }
109}
110
111impl std::error::Error for CollectiveError {}
112
113/// Per-rank symmetric memory: a `Vec<u8>` per rank, all the same
114/// size. Owned by the [`LocalTransport`].
115#[derive(Debug)]
116pub struct SymmetricHeap {
117    storage: Vec<Arc<RwLock<Vec<u8>>>>,
118    pub heap_size: usize,
119}
120
121impl SymmetricHeap {
122    pub fn new(num_ranks: u32, heap_size: usize) -> Self {
123        let storage = (0..num_ranks)
124            .map(|_| Arc::new(RwLock::new(vec![0u8; heap_size])))
125            .collect();
126        Self { storage, heap_size }
127    }
128
129    pub fn num_ranks(&self) -> u32 {
130        self.storage.len() as u32
131    }
132
133    pub fn rank_view(&self, rank: Rank) -> Option<Arc<RwLock<Vec<u8>>>> {
134        self.storage.get(rank.0 as usize).cloned()
135    }
136}
137
138/// Single-machine in-process transport. All `num_ranks`
139/// "ranks" share one [`SymmetricHeap`] instance, so put / get
140/// are just locks + memcpy. Useful for unit tests and for
141/// algorithm-correctness checking of collective ops without a
142/// real cluster.
143#[derive(Debug, Clone)]
144pub struct LocalTransport {
145    heap: Arc<SymmetricHeap>,
146    me: Rank,
147    /// Shared, reusable rendezvous barrier across all ranks from the same
148    /// [`fan_out`](Self::fan_out). A real `std::sync::Barrier` (auto-reset
149    /// after every rank passes), so multi-step collectives — ring all-reduce
150    /// in particular — synchronize correctly across threads.
151    barrier: Arc<std::sync::Barrier>,
152}
153
154impl LocalTransport {
155    pub fn new(num_ranks: u32, heap_size: usize, this_rank: Rank) -> Self {
156        let heap = Arc::new(SymmetricHeap::new(num_ranks, heap_size));
157        let barrier = Arc::new(std::sync::Barrier::new(num_ranks as usize));
158        Self::with_heap(heap, this_rank, barrier)
159    }
160
161    /// Construct multiple `LocalTransport`s sharing one heap **and one
162    /// barrier** — `Vec` of length `num_ranks`, each with its own `me`.
163    /// Drive each on its own thread for multi-rank collective tests.
164    pub fn fan_out(num_ranks: u32, heap_size: usize) -> Vec<Self> {
165        let heap = Arc::new(SymmetricHeap::new(num_ranks, heap_size));
166        let barrier = Arc::new(std::sync::Barrier::new(num_ranks as usize));
167        (0..num_ranks)
168            .map(|i| Self::with_heap(heap.clone(), Rank(i), barrier.clone()))
169            .collect()
170    }
171
172    fn with_heap(heap: Arc<SymmetricHeap>, me: Rank, barrier: Arc<std::sync::Barrier>) -> Self {
173        Self { heap, me, barrier }
174    }
175
176    fn check_buf(&self, buf: SymmetricBuffer) -> Result<(), CollectiveError> {
177        if buf.rank.0 >= self.heap.num_ranks() {
178            return Err(CollectiveError::UnknownRank {
179                rank: buf.rank,
180                num_ranks: self.heap.num_ranks(),
181            });
182        }
183        if buf.offset + buf.len > self.heap.heap_size {
184            return Err(CollectiveError::OutOfBounds {
185                rank: buf.rank,
186                offset: buf.offset,
187                len: buf.len,
188                heap_size: self.heap.heap_size,
189            });
190        }
191        Ok(())
192    }
193}
194
195impl SymmetricTransport for LocalTransport {
196    fn num_ranks(&self) -> u32 {
197        self.heap.num_ranks()
198    }
199    fn this_rank(&self) -> Rank {
200        self.me
201    }
202
203    fn put(&self, buf: SymmetricBuffer, src: &[u8]) -> Result<(), CollectiveError> {
204        self.check_buf(buf)?;
205        if src.len() != buf.len {
206            return Err(CollectiveError::LengthMismatch {
207                expected: buf.len,
208                got: src.len(),
209            });
210        }
211        let view = self.heap.rank_view(buf.rank).expect("checked above");
212        let mut guard = view.write().unwrap();
213        guard[buf.offset..buf.offset + buf.len].copy_from_slice(src);
214        Ok(())
215    }
216
217    fn get(&self, buf: SymmetricBuffer, dst: &mut [u8]) -> Result<(), CollectiveError> {
218        self.check_buf(buf)?;
219        if dst.len() != buf.len {
220            return Err(CollectiveError::LengthMismatch {
221                expected: buf.len,
222                got: dst.len(),
223            });
224        }
225        let view = self.heap.rank_view(buf.rank).expect("checked above");
226        let guard = view.read().unwrap();
227        dst.copy_from_slice(&guard[buf.offset..buf.offset + buf.len]);
228        Ok(())
229    }
230
231    fn barrier(&self) -> Result<(), CollectiveError> {
232        // Real rendezvous: blocks until every rank from the same `fan_out`
233        // reaches here, then all proceed. Reusable across collective steps.
234        self.barrier.wait();
235        Ok(())
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn put_then_get_round_trips() {
245        let t = LocalTransport::new(4, 1024, Rank(0));
246        let buf = SymmetricBuffer {
247            rank: Rank(2),
248            offset: 16,
249            len: 8,
250        };
251        t.put(buf, &[1, 2, 3, 4, 5, 6, 7, 8]).unwrap();
252        let mut dst = [0u8; 8];
253        t.get(buf, &mut dst).unwrap();
254        assert_eq!(&dst, &[1, 2, 3, 4, 5, 6, 7, 8]);
255    }
256
257    #[test]
258    fn fan_out_yields_one_transport_per_rank() {
259        let ts = LocalTransport::fan_out(3, 64);
260        assert_eq!(ts.len(), 3);
261        for (i, t) in ts.iter().enumerate() {
262            assert_eq!(t.this_rank(), Rank(i as u32));
263            assert_eq!(t.num_ranks(), 3);
264        }
265    }
266
267    #[test]
268    fn put_visible_to_other_rank_via_shared_heap() {
269        // Rank 0 writes into rank 2's slot; rank 2 reads its
270        // own slot and sees the write.
271        let ts = LocalTransport::fan_out(3, 32);
272        let payload = [9u8, 9, 9, 9];
273        ts[0]
274            .put(
275                SymmetricBuffer {
276                    rank: Rank(2),
277                    offset: 0,
278                    len: 4,
279                },
280                &payload,
281            )
282            .unwrap();
283        let mut dst = [0u8; 4];
284        ts[2]
285            .get(
286                SymmetricBuffer {
287                    rank: Rank(2),
288                    offset: 0,
289                    len: 4,
290                },
291                &mut dst,
292            )
293            .unwrap();
294        assert_eq!(dst, payload);
295    }
296
297    #[test]
298    fn oob_offset_errors() {
299        let t = LocalTransport::new(2, 8, Rank(0));
300        let err = t
301            .put(
302                SymmetricBuffer {
303                    rank: Rank(1),
304                    offset: 4,
305                    len: 8,
306                },
307                &[0u8; 8],
308            )
309            .unwrap_err();
310        assert!(matches!(err, CollectiveError::OutOfBounds { .. }));
311    }
312
313    #[test]
314    fn unknown_rank_errors() {
315        let t = LocalTransport::new(2, 8, Rank(0));
316        let err = t
317            .get(
318                SymmetricBuffer {
319                    rank: Rank(99),
320                    offset: 0,
321                    len: 4,
322                },
323                &mut [0u8; 4],
324            )
325            .unwrap_err();
326        assert!(matches!(err, CollectiveError::UnknownRank { .. }));
327    }
328
329    #[test]
330    fn length_mismatch_errors() {
331        let t = LocalTransport::new(2, 32, Rank(0));
332        let err = t
333            .put(
334                SymmetricBuffer {
335                    rank: Rank(1),
336                    offset: 0,
337                    len: 4,
338                },
339                &[0u8; 8],
340            )
341            .unwrap_err();
342        assert!(matches!(err, CollectiveError::LengthMismatch { .. }));
343    }
344}