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}