1use anyhow::{Context, Result, ensure};
11use std::ffi::c_void;
12use std::ptr;
13use std::sync::atomic::Ordering;
14use std::time::{Duration, Instant};
15
16use super::{ALL_REDUCE_DTYPE_BYTES, COLLECTIVE_TIMEOUT_SECS, NcclBackend};
17use crate::CommBackend;
18use crate::collective_diagnostics::Dtype;
19use metrale_gpu_sys::nccl::{self, NcclDataType, NcclRedOp};
20
21impl CommBackend for NcclBackend {
22 fn all_reduce(&self, ptr: u64, bytes: usize) -> Result<()> {
23 self.begin_submission("all_reduce", Dtype::Bf16, bytes, self.legacy_stream, None)?;
24 if self.world_size == 2 && self.add_kernel.load(Ordering::Relaxed) != 0 {
25 return self.all_reduce_2rank(ptr, bytes, self.legacy_stream);
26 }
27 let count = bytes / ALL_REDUCE_DTYPE_BYTES;
30 let comm = *self.comm.lock();
31 let result = unsafe {
32 nccl::ncclAllReduce(
33 ptr as *const _,
34 ptr as *mut _,
35 count,
36 NcclDataType::Bfloat16,
37 NcclRedOp::Sum,
38 comm,
39 self.legacy_stream,
40 )
41 };
42 nccl::check_nccl(result, "ncclAllReduce")?;
43 self.check_async_error(comm);
44 Ok(())
45 }
46
47 fn all_reduce_async(&self, ptr: u64, bytes: usize, compute_stream: u64) -> Result<()> {
48 self.begin_submission(
49 "all_reduce_async",
50 Dtype::Bf16,
51 bytes,
52 self.comm_stream,
53 None,
54 )?;
55 if self.world_size == 2 && self.add_kernel.load(Ordering::Relaxed) != 0 {
56 nccl::record_event(self.compute_done_event, compute_stream)?;
57 nccl::stream_wait_event(self.comm_stream, self.compute_done_event)?;
58
59 self.all_reduce_2rank(ptr, bytes, self.comm_stream)?;
60
61 nccl::record_event(self.comm_done_event, self.comm_stream)?;
62 nccl::stream_wait_event(compute_stream, self.comm_done_event)?;
63 return Ok(());
64 }
65
66 let count = bytes / ALL_REDUCE_DTYPE_BYTES;
68 let comm = *self.comm.lock();
69
70 nccl::record_event(self.compute_done_event, compute_stream)?;
71
72 nccl::stream_wait_event(self.comm_stream, self.compute_done_event)?;
73
74 let result = unsafe {
75 nccl::ncclAllReduce(
76 ptr as *const _,
77 ptr as *mut _,
78 count,
79 NcclDataType::Bfloat16,
80 NcclRedOp::Sum,
81 comm,
82 self.comm_stream,
83 )
84 };
85 nccl::check_nccl(result, "ncclAllReduce (async)")?;
86
87 nccl::record_event(self.comm_done_event, self.comm_stream)?;
88
89 nccl::stream_wait_event(compute_stream, self.comm_done_event)?;
90
91 self.check_async_error(comm);
92
93 Ok(())
94 }
95
96 fn register_buffer(&self, ptr: u64, bytes: usize) -> Result<u64> {
97 let mut handle: *mut c_void = ptr::null_mut();
98 let comm = *self.comm.lock();
99 let result =
100 unsafe { nccl::ncclCommRegister(comm, ptr as *mut c_void, bytes, &mut handle) };
101 nccl::check_nccl(result, "ncclCommRegister")?;
102 self.registered_handles.lock().push(handle);
103 Ok(handle as u64)
104 }
105
106 fn deregister_buffer(&self, handle: u64) -> Result<()> {
107 let comm = *self.comm.lock();
108 let result = unsafe { nccl::ncclCommDeregister(comm, handle as *mut c_void) };
109 nccl::check_nccl(result, "ncclCommDeregister")
110 }
111
112 fn symmetric_alloc(&self, bytes: usize) -> Result<u64> {
113 let ptr = unsafe { nccl::nccl_mem_alloc(bytes) }?;
116 Ok(ptr as u64)
117 }
118
119 fn symmetric_free(&self, ptr: u64) -> Result<()> {
120 unsafe { nccl::nccl_mem_free(ptr as *mut c_void) }
123 }
124
125 fn set_add_kernel(&self, handle: u64) {
126 self.add_kernel.store(handle, Ordering::Relaxed);
127 tracing::info!(
128 "NCCL backend: bf16_add_inplace kernel set \
129 (2-rank send/recv enabled)"
130 );
131 }
132
133 fn all_gather(&self, send_ptr: u64, recv_ptr: u64, bytes: usize) -> Result<()> {
134 self.begin_submission("all_gather", Dtype::U8, bytes, self.legacy_stream, None)?;
135 let comm = *self.comm.lock();
136 let result = unsafe {
137 nccl::ncclAllGather(
138 send_ptr as *const c_void,
139 recv_ptr as *mut c_void,
140 bytes,
141 NcclDataType::Uint8,
142 comm,
143 self.legacy_stream,
144 )
145 };
146 nccl::check_nccl(result, "ncclAllGather")?;
147 self.check_async_error(comm);
148 Ok(())
149 }
150
151 fn reduce_scatter(&self, send_ptr: u64, recv_ptr: u64, bytes: usize) -> Result<()> {
152 self.begin_submission("reduce_scatter", Dtype::U8, bytes, self.legacy_stream, None)?;
153 let comm = *self.comm.lock();
154 let result = unsafe {
155 nccl::ncclReduceScatter(
156 send_ptr as *const c_void,
157 recv_ptr as *mut c_void,
158 bytes,
159 NcclDataType::Uint8,
160 NcclRedOp::Sum,
161 comm,
162 self.legacy_stream,
163 )
164 };
165 nccl::check_nccl(result, "ncclReduceScatter")?;
166 self.check_async_error(comm);
167 Ok(())
168 }
169
170 fn broadcast(&self, ptr: u64, bytes: usize, root: usize) -> Result<()> {
171 self.broadcast_with_wait(ptr, bytes, root, false)
172 }
173
174 fn recv_command_u32(&self, ptr: u64, root: usize) -> Result<()> {
175 ensure!(
176 self.rank != root,
177 "idle command receive requires non-root rank"
178 );
179 self.broadcast_with_wait(ptr, 4, root, true)
180 }
181
182 fn barrier(&self) -> Result<()> {
183 self.begin_submission("barrier", Dtype::F32, 0, self.legacy_stream, None)?;
184 let comm = *self.comm.lock();
185 let result = unsafe {
186 nccl::ncclAllReduce(
187 ptr::null(),
188 ptr::null_mut(),
189 0,
190 NcclDataType::Float32,
191 NcclRedOp::Sum,
192 comm,
193 self.legacy_stream,
194 )
195 };
196 nccl::check_nccl(result, "barrier (ncclAllReduce count=0)")?;
197 self.check_async_error(comm);
198 Ok(())
199 }
200
201 fn send_to(&self, ptr: u64, bytes: usize, dest_rank: usize, stream: u64) -> Result<()> {
202 self.begin_submission("send", Dtype::U8, bytes, stream, Some(dest_rank))?;
203 let comm = *self.comm.lock();
204 let result = unsafe {
205 nccl::ncclSend(
206 ptr as *const c_void,
207 bytes,
208 NcclDataType::Uint8,
209 dest_rank as i32,
210 comm,
211 stream,
212 )
213 };
214 nccl::check_nccl(result, "ncclSend (send_to)")
215 }
216
217 fn recv_from(&self, ptr: u64, bytes: usize, src_rank: usize, stream: u64) -> Result<()> {
218 self.begin_submission("recv", Dtype::U8, bytes, stream, Some(src_rank))?;
219 let comm = *self.comm.lock();
220 let result = unsafe {
221 nccl::ncclRecv(
222 ptr as *mut c_void,
223 bytes,
224 NcclDataType::Uint8,
225 src_rank as i32,
226 comm,
227 stream,
228 )
229 };
230 nccl::check_nccl(result, "ncclRecv (recv_from)")
231 }
232
233 fn group_start(&self) -> Result<()> {
234 let result = unsafe { nccl::ncclGroupStart() };
235 nccl::check_nccl(result, "ncclGroupStart")
236 }
237
238 fn group_end(&self) -> Result<()> {
239 let result = unsafe { nccl::ncclGroupEnd() };
240 nccl::check_nccl(result, "ncclGroupEnd")
241 }
242
243 fn is_healthy(&self) -> bool {
244 if self.unhealthy.load(Ordering::Acquire) {
245 return false;
246 }
247 let comm = *self.comm.lock();
248 self.check_async_error(comm)
249 }
250
251 fn attempt_reconnect(&self) -> Result<()> {
252 self.reconnect_inner()
253 }
254
255 fn rank(&self) -> usize {
256 self.rank
257 }
258
259 fn world_size(&self) -> usize {
260 self.world_size
261 }
262}
263
264impl NcclBackend {
265 fn broadcast_with_wait(
266 &self,
267 ptr: u64,
268 bytes: usize,
269 root: usize,
270 idle_command: bool,
271 ) -> Result<()> {
272 self.begin_submission(
273 "broadcast",
274 Dtype::U8,
275 bytes,
276 self.legacy_stream,
277 Some(root),
278 )?;
279 let start = Instant::now();
280 let comm = *self.comm.lock();
281
282 let result = unsafe {
283 nccl::ncclBroadcast(
284 ptr as *const _,
285 ptr as *mut _,
286 bytes,
287 NcclDataType::Uint8,
288 root as i32,
289 comm,
290 self.legacy_stream,
291 )
292 };
293 nccl::check_nccl(result, "ncclBroadcast")?;
294
295 let ready = || {
298 ensure!(self.check_async_error(comm), "NCCL asynchronous failure");
299 nccl::stream_ready(self.legacy_stream)
300 };
301 let pause = || std::thread::sleep(Duration::from_millis(1));
302 let completion = if idle_command {
303 crate::collective_wait::poll_idle_command(ready, pause)
304 } else {
305 crate::collective_wait::poll_completion(
306 Duration::from_secs(COLLECTIVE_TIMEOUT_SECS),
307 || start.elapsed(),
308 ready,
309 pause,
310 )
311 };
312 crate::collective_wait::poison_on_error(completion, &self.unhealthy).with_context(
313 || {
314 format!(
315 "NCCL broadcast rank={} world_size={} root={root} bytes={bytes}; \
316 communicator poisoned; stop all ranks before retrying",
317 self.rank, self.world_size,
318 )
319 },
320 )?;
321
322 Ok(())
323 }
324
325 fn begin_submission(
326 &self,
327 op: &str,
328 dtype: Dtype,
329 bytes: usize,
330 stream: u64,
331 peer: Option<usize>,
332 ) -> Result<()> {
333 crate::collective_wait::ensure_healthy(&self.unhealthy, self.rank, self.world_size, op)?;
334 self.diagnostics.submit(op, dtype, bytes, stream, peer)
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 #[allow(clippy::assertions_on_constants)]
344 fn test_collective_timeout_constant() {
345 assert!(COLLECTIVE_TIMEOUT_SECS >= 10);
346 assert!(COLLECTIVE_TIMEOUT_SECS <= 300);
347 }
348}