metrale_comm/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#![deny(warnings)]
4#![deny(clippy::all)]
5
6//! 2026-09-26: Collective-communication backends behind one trait,
7//! [`CommBackend`]: [`SingleGpuBackend`], where every operation is a no-op,
8//! and, with the `nccl` feature, `NcclBackend`. No other crate calls NCCL.
9//!
10//! Owner: metrale-comm.
11//! Invariants: none beyond the types.
12
13use anyhow::Result;
14
15// 2026-09-26: Gated on `nccl` because the bindings link libnccl. The release
16// targets built with `--features cuda` alone, such as the AMD SCALE one, have
17// no NCCL library.
18#[cfg(feature = "nccl")]
19mod collective_diagnostics;
20#[cfg(feature = "nccl")]
21mod collective_wait;
22#[cfg(feature = "nccl")]
23pub mod nccl_backend;
24#[cfg(feature = "nccl")]
25pub use nccl_backend::NcclBackend;
26
27/// 2026-09-26: Collective and point-to-point operations on raw device
28/// pointers and byte counts. A pointer is a `u64` (a CUDA `CUdeviceptr`), so
29/// this crate does not depend on metrale-gpu-runtime.
30pub trait CommBackend: Send + Sync {
31    /// 2026-09-26: Sum `bytes` at `ptr` across all ranks, in place; every rank
32    /// gets the result. `NcclBackend` reduces BF16 elements.
33    fn all_reduce(&self, ptr: u64, bytes: usize) -> Result<()>;
34
35    /// 2026-09-26: Each rank contributes `bytes` from `send_ptr`; every rank
36    /// receives all contributions, in rank order, at `recv_ptr`.
37    fn all_gather(&self, send_ptr: u64, recv_ptr: u64, bytes: usize) -> Result<()>;
38
39    /// 2026-09-26: Sum across ranks and give each rank its `bytes`-sized share
40    /// at `recv_ptr`. `NcclBackend` sums `u8` elements.
41    fn reduce_scatter(&self, send_ptr: u64, recv_ptr: u64, bytes: usize) -> Result<()>;
42
43    /// 2026-09-26: Copy `bytes` at `ptr` from rank `root` to every rank, in
44    /// place.
45    fn broadcast(&self, ptr: u64, bytes: usize, root: usize) -> Result<()>;
46
47    /// 2026-09-26: On a non-root rank, receive the first `u32` of the next
48    /// worker command from `root`, the receive side of a 4-byte `broadcast`.
49    /// Unlike `broadcast`, it may wait without a deadline, so an implementation
50    /// must keep checking for transport errors while it waits. The command's
51    /// later words use `broadcast`. The default refuses `root` and otherwise
52    /// calls `broadcast(ptr, 4, root)`.
53    fn recv_command_u32(&self, ptr: u64, root: usize) -> Result<()> {
54        anyhow::ensure!(
55            self.rank() != root,
56            "idle command receive requires non-root rank"
57        );
58        self.broadcast(ptr, 4, root)
59    }
60
61    /// 2026-09-26: A barrier across ranks. `NcclBackend` enqueues a zero-count
62    /// all-reduce on its stream and returns without waiting for it.
63    fn barrier(&self) -> Result<()>;
64
65    /// 2026-09-26: All-reduce ordered against `compute_stream` on the device:
66    /// the reduce starts after the work already queued there and later work
67    /// there waits for it. The host does not wait. The default ignores
68    /// `compute_stream` and calls `all_reduce`.
69    fn all_reduce_async(&self, ptr: u64, bytes: usize, compute_stream: u64) -> Result<()> {
70        let _ = compute_stream;
71        self.all_reduce(ptr, bytes)
72    }
73
74    /// 2026-09-26: Register a device buffer with the backend and return an
75    /// opaque handle (`ncclCommRegister` in `NcclBackend`). The default
76    /// registers nothing and returns 0.
77    fn register_buffer(&self, _ptr: u64, _bytes: usize) -> Result<u64> {
78        Ok(0)
79    }
80
81    /// 2026-09-26: Deregister a handle from `register_buffer`.
82    fn deregister_buffer(&self, _handle: u64) -> Result<()> {
83        Ok(())
84    }
85
86    /// 2026-09-26: Allocate `bytes` of device memory from the backend's
87    /// allocator (`ncclMemAlloc` in `NcclBackend`) and return the pointer. The
88    /// default returns `Err`.
89    fn symmetric_alloc(&self, _bytes: usize) -> Result<u64> {
90        anyhow::bail!("symmetric_alloc not supported by this CommBackend");
91    }
92
93    /// 2026-09-26: Free a pointer from `symmetric_alloc`. The default returns
94    /// `Err`.
95    fn symmetric_free(&self, _ptr: u64) -> Result<()> {
96        anyhow::bail!("symmetric_free not supported by this CommBackend");
97    }
98
99    /// 2026-09-26: Hand the backend the `bf16_add_inplace` kernel handle. The
100    /// model engine looks it up and passes it after registering its buffers.
101    /// `NcclBackend` takes its 2-rank send/recv all-reduce path only once it
102    /// is set.
103    fn set_add_kernel(&self, _handle: u64) {
104        // 2026-09-26: The default keeps no kernel.
105    }
106
107    /// 2026-09-26: Send `bytes` at `ptr` to `dest_rank`, enqueued on `stream`.
108    /// Pairs with a `recv_from` of the same size on `dest_rank`.
109    fn send_to(&self, ptr: u64, bytes: usize, dest_rank: usize, stream: u64) -> Result<()>;
110
111    /// 2026-09-26: Receive `bytes` into `ptr` from `src_rank`, enqueued on
112    /// `stream`. Pairs with a `send_to` of the same size on `src_rank`.
113    fn recv_from(&self, ptr: u64, bytes: usize, src_rank: usize, stream: u64) -> Result<()>;
114
115    /// 2026-09-26: Open a group: `NcclBackend` calls `ncclGroupStart`, so the
116    /// operations up to `group_end` are submitted together. The default does
117    /// nothing.
118    fn group_start(&self) -> Result<()> {
119        Ok(())
120    }
121
122    /// 2026-09-26: Close the group opened by `group_start`.
123    fn group_end(&self) -> Result<()> {
124        Ok(())
125    }
126
127    /// 2026-09-26: `false` after the backend has seen a transport error or a
128    /// completion timeout, until a successful `attempt_reconnect`.
129    /// `NcclBackend` also queries `ncclCommGetAsyncError`. The default returns
130    /// `true`.
131    fn is_healthy(&self) -> bool {
132        true
133    }
134
135    /// 2026-09-26: Replace a failed communicator. `NcclBackend` aborts it and
136    /// repeats the TCP bootstrap, so every rank must call this at the same
137    /// time. The default does nothing.
138    fn attempt_reconnect(&self) -> Result<()> {
139        Ok(())
140    }
141
142    /// 2026-09-26: This rank's index, from 0.
143    fn rank(&self) -> usize;
144
145    /// 2026-09-26: Number of ranks.
146    fn world_size(&self) -> usize;
147}
148
149/// 2026-09-26: One rank: every collective and point-to-point operation
150/// returns `Ok` without doing anything, `rank` is 0 and `world_size` is 1.
151pub struct SingleGpuBackend;
152
153impl CommBackend for SingleGpuBackend {
154    fn all_reduce(&self, _ptr: u64, _bytes: usize) -> Result<()> {
155        Ok(())
156    }
157
158    fn all_gather(&self, _send_ptr: u64, _recv_ptr: u64, _bytes: usize) -> Result<()> {
159        Ok(())
160    }
161
162    fn reduce_scatter(&self, _send_ptr: u64, _recv_ptr: u64, _bytes: usize) -> Result<()> {
163        Ok(())
164    }
165
166    fn broadcast(&self, _ptr: u64, _bytes: usize, _root: usize) -> Result<()> {
167        Ok(())
168    }
169
170    fn barrier(&self) -> Result<()> {
171        Ok(())
172    }
173
174    fn send_to(&self, _ptr: u64, _bytes: usize, _dest_rank: usize, _stream: u64) -> Result<()> {
175        Ok(())
176    }
177
178    fn recv_from(&self, _ptr: u64, _bytes: usize, _src_rank: usize, _stream: u64) -> Result<()> {
179        Ok(())
180    }
181
182    fn rank(&self) -> usize {
183        0
184    }
185
186    fn world_size(&self) -> usize {
187        1
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_single_gpu_noop() {
197        let comm = SingleGpuBackend;
198        assert_eq!(comm.rank(), 0);
199        assert_eq!(comm.world_size(), 1);
200        comm.all_reduce(0x1000, 1024).unwrap();
201        comm.all_reduce_async(0x1000, 1024, 0x3000).unwrap();
202        comm.all_gather(0x1000, 0x2000, 512).unwrap();
203        comm.reduce_scatter(0x1000, 0x2000, 512).unwrap();
204        comm.broadcast(0x1000, 256, 0).unwrap();
205        comm.barrier().unwrap();
206        comm.send_to(0x1000, 256, 0, 0).unwrap();
207        comm.recv_from(0x2000, 256, 0, 0).unwrap();
208        comm.group_start().unwrap();
209        comm.group_end().unwrap();
210        let registration = comm.register_buffer(0x1000, 1024).unwrap();
211        assert_eq!(registration, 0, "single-GPU registration is a no-op handle");
212        comm.deregister_buffer(registration).unwrap();
213        comm.set_add_kernel(0x4000);
214        assert!(comm.is_healthy());
215        comm.attempt_reconnect().unwrap();
216    }
217
218    #[test]
219    fn test_single_gpu_symmetric_alloc_unsupported() {
220        // 2026-09-26: `SingleGpuBackend` keeps the trait defaults, which
221        // return `Err`.
222        let comm = SingleGpuBackend;
223        assert!(comm.symmetric_alloc(1024).is_err());
224        assert!(comm.symmetric_free(0x1000).is_err());
225    }
226}