1use anyhow::{Context, Result};
29use parking_lot::Mutex;
30use std::ffi::c_void;
31use std::io::{Read, Write};
32use std::net::{TcpListener, TcpStream};
33use std::ptr;
34use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
35
36use metrale_gpu_sys::nccl::{self, NcclComm, NcclDataType, NcclResult, NcclUniqueId};
37
38unsafe extern "C" {
40 fn cuMemAlloc_v2(dptr: *mut u64, bytesize: usize) -> i32;
41 fn cuMemFree_v2(dptr: u64) -> i32;
42 fn cuLaunchKernel(
43 f: u64,
44 gridDimX: u32,
45 gridDimY: u32,
46 gridDimZ: u32,
47 blockDimX: u32,
48 blockDimY: u32,
49 blockDimZ: u32,
50 sharedMemBytes: u32,
51 hStream: u64,
52 kernelParams: *mut *mut c_void,
53 extra: *mut *mut c_void,
54 ) -> i32;
55}
56
57mod recv_buffer;
58use recv_buffer::ensure_payload_fits;
59pub use recv_buffer::{ALL_REDUCE_DTYPE_BYTES, required_model_recv_bytes, required_recv_bytes};
60
61pub(super) const COLLECTIVE_TIMEOUT_SECS: u64 = 30;
64
65pub struct NcclBackend {
68 comm: Mutex<NcclComm>,
72 diagnostics: crate::collective_diagnostics::Diagnostics,
73 rank: usize,
74 world_size: usize,
75 comm_stream: u64,
77 compute_done_event: u64,
80 comm_done_event: u64,
82 legacy_stream: u64,
85 recv_buffer: u64,
88 recv_capacity: usize,
90 registered_handles: Mutex<Vec<*mut c_void>>,
93 add_kernel: AtomicU64,
96 unhealthy: AtomicBool,
99 reconnect_count: AtomicU64,
101 master_addr: String,
103 master_port: u16,
104}
105
106unsafe impl Send for NcclBackend {}
112unsafe impl Sync for NcclBackend {}
113
114impl NcclBackend {
115 pub fn new(
128 rank: usize,
129 world_size: usize,
130 master_addr: &str,
131 master_port: u16,
132 stream: u64,
133 recv_capacity: usize,
134 ) -> Result<Self> {
135 let diagnostic_env = std::env::var(crate::collective_diagnostics::ENV).ok();
136 let diagnostics = crate::collective_diagnostics::Diagnostics::new(
137 diagnostic_env.as_deref(),
138 rank,
139 world_size,
140 )?;
141 Self::log_nccl_env_vars();
142
143 let unique_id = if rank == 0 {
144 let id = Self::generate_unique_id()?;
145 Self::distribute_id(&id, master_addr, master_port, world_size)?;
146 id
147 } else {
148 Self::receive_id(master_addr, master_port)?
149 };
150
151 let mut comm: NcclComm = ptr::null_mut();
152 let result =
153 unsafe { nccl::ncclCommInitRank(&mut comm, world_size as i32, unique_id, rank as i32) };
154 nccl::check_nccl(result, "ncclCommInitRank")?;
155
156 let comm_stream = nccl::create_stream()?;
157 let compute_done_event = nccl::create_event()?;
158 let comm_done_event = nccl::create_event()?;
159
160 let mut recv_buffer: u64 = 0;
161 if world_size == 2 {
162 if recv_capacity == 0 {
163 anyhow::bail!(
164 "world_size == 2 requires a non-zero receive-buffer capacity; \
165 compute it with required_recv_bytes(max_batch_tokens, hidden_size, \
166 ALL_REDUCE_DTYPE_BYTES)"
167 );
168 }
169 let status = unsafe { cuMemAlloc_v2(&mut recv_buffer, recv_capacity) };
170 if status != 0 {
171 anyhow::bail!(
172 "cuMemAlloc_v2 for recv_buffer ({recv_capacity} bytes) failed: status {status}"
173 );
174 }
175 let mut handle: *mut c_void = ptr::null_mut();
176 let result = unsafe {
177 nccl::ncclCommRegister(comm, recv_buffer as *mut c_void, recv_capacity, &mut handle)
178 };
179 if result != NcclResult::Success {
180 tracing::warn!("ncclCommRegister for recv_buffer failed (non-fatal): {result:?}");
181 } else {
182 tracing::info!(
183 "Registered recv_buffer ({} KB) with NCCL",
184 recv_capacity / 1024
185 );
186 }
187 }
188
189 Ok(Self {
190 comm: Mutex::new(comm),
191 diagnostics,
192 rank,
193 world_size,
194 comm_stream,
195 compute_done_event,
196 comm_done_event,
197 legacy_stream: stream,
198 recv_buffer,
199 recv_capacity: if world_size == 2 { recv_capacity } else { 0 },
200 registered_handles: Mutex::new(Vec::new()),
201 add_kernel: AtomicU64::new(0),
202 unhealthy: AtomicBool::new(false),
203 reconnect_count: AtomicU64::new(0),
204 master_addr: master_addr.to_owned(),
205 master_port,
206 })
207 }
208
209 fn log_nccl_env_vars() {
212 let vars = [
213 "NCCL_TIMEOUT",
214 "NCCL_WATCHDOG_TIMEOUT",
215 "NCCL_IB_TIMEOUT",
216 "NCCL_IB_RETRY_CNT",
217 "NCCL_SOCKET_IFNAME",
218 "NCCL_DEBUG",
219 ];
220 for var in &vars {
221 match std::env::var(var) {
222 Ok(val) => tracing::info!("NCCL env: {var}={val}"),
223 Err(_) => tracing::debug!("NCCL env: {var} not set"),
224 }
225 }
226 }
227
228 fn check_async_error(&self, comm: NcclComm) -> bool {
232 let mut async_err = NcclResult::Success;
233 let result = unsafe { nccl::ncclCommGetAsyncError(comm, &mut async_err) };
234 if result != NcclResult::Success {
235 tracing::error!(
236 "ncclCommGetAsyncError call itself failed: {result:?} \
237 — marking unhealthy"
238 );
239 self.unhealthy.store(true, Ordering::Release);
240 return false;
241 }
242 if async_err != NcclResult::Success {
243 tracing::error!("NCCL async error detected: {async_err:?} — marking unhealthy");
244 self.unhealthy.store(true, Ordering::Release);
245 return false;
246 }
247 true
248 }
249
250 fn reconnect_inner(&self) -> Result<()> {
255 let mut comm_guard = self.comm.lock();
256
257 if !self.unhealthy.load(Ordering::Acquire) {
260 tracing::info!("NCCL communicator already recovered by another thread");
261 return Ok(());
262 }
263
264 let old_comm = *comm_guard;
265 let attempt = self.reconnect_count.load(Ordering::Relaxed) + 1;
266 tracing::warn!(
267 "NCCL reconnect: aborting old communicator \
268 (rank={}, reconnect #{})",
269 self.rank,
270 attempt,
271 );
272
273 if !old_comm.is_null() {
274 let result = unsafe { nccl::ncclCommAbort(old_comm) };
275 if result != NcclResult::Success {
276 tracing::warn!("ncclCommAbort returned {result:?} (proceeding anyway)");
277 }
278 }
279
280 let reconnect_port = self.master_port.wrapping_add(1);
283 let unique_id = if self.rank == 0 {
284 let id = Self::generate_unique_id()?;
285 Self::distribute_id(&id, &self.master_addr, reconnect_port, self.world_size)?;
286 id
287 } else {
288 Self::receive_id(&self.master_addr, reconnect_port)?
289 };
290
291 let mut new_comm: NcclComm = ptr::null_mut();
292 let result = unsafe {
293 nccl::ncclCommInitRank(
294 &mut new_comm,
295 self.world_size as i32,
296 unique_id,
297 self.rank as i32,
298 )
299 };
300 nccl::check_nccl(result, "ncclCommInitRank (reconnect)")?;
301
302 if self.world_size == 2 && self.recv_buffer != 0 {
303 let mut handle: *mut c_void = ptr::null_mut();
304 let result = unsafe {
305 nccl::ncclCommRegister(
306 new_comm,
307 self.recv_buffer as *mut c_void,
308 self.recv_capacity,
309 &mut handle,
310 )
311 };
312 if result != NcclResult::Success {
313 tracing::warn!(
314 "ncclCommRegister for recv_buffer after reconnect \
315 failed: {result:?}"
316 );
317 } else {
318 tracing::info!("Re-registered recv_buffer after reconnect");
319 }
320 }
321
322 let mut handles = self.registered_handles.lock();
326 if !handles.is_empty() {
327 tracing::warn!(
328 "Clearing {} stale registered buffer handles after reconnect",
329 handles.len()
330 );
331 handles.clear();
332 }
333 drop(handles);
334
335 *comm_guard = new_comm;
336 self.unhealthy.store(false, Ordering::Release);
337 self.reconnect_count.fetch_add(1, Ordering::Relaxed);
338
339 tracing::info!(
340 "NCCL reconnect successful (rank={}, total reconnects={})",
341 self.rank,
342 self.reconnect_count.load(Ordering::Relaxed),
343 );
344
345 Ok(())
346 }
347
348 fn all_reduce_2rank(&self, ptr: u64, bytes: usize, stream: u64) -> Result<()> {
353 ensure_payload_fits(bytes, self.recv_capacity, self.rank, self.world_size)?;
354
355 if bytes == 0 {
358 return Ok(());
359 }
360
361 let count = bytes / ALL_REDUCE_DTYPE_BYTES;
362 let partner = (1 - self.rank) as i32;
363 let comm = *self.comm.lock();
364
365 let result = unsafe { nccl::ncclGroupStart() };
366 nccl::check_nccl(result, "ncclGroupStart")?;
367
368 let result = unsafe {
369 nccl::ncclSend(
370 ptr as *const c_void,
371 count,
372 NcclDataType::Bfloat16,
373 partner,
374 comm,
375 stream,
376 )
377 };
378 nccl::check_nccl(result, "ncclSend")?;
379
380 let result = unsafe {
381 nccl::ncclRecv(
382 self.recv_buffer as *mut c_void,
383 count,
384 NcclDataType::Bfloat16,
385 partner,
386 comm,
387 stream,
388 )
389 };
390 nccl::check_nccl(result, "ncclRecv")?;
391
392 let result = unsafe { nccl::ncclGroupEnd() };
393 nccl::check_nccl(result, "ncclGroupEnd")?;
394
395 self.check_async_error(comm);
396
397 let kernel = self.add_kernel.load(Ordering::Relaxed);
398 if kernel != 0 {
399 let threads: u32 = 256;
400 let blocks: u32 = (count as u32).div_ceil(threads);
401 let mut p_dst = ptr;
402 let mut p_src = self.recv_buffer;
403 let mut p_n = count as i32;
404 let mut params: [*mut c_void; 3] = [
405 &mut p_dst as *mut u64 as *mut c_void,
406 &mut p_src as *mut u64 as *mut c_void,
407 &mut p_n as *mut i32 as *mut c_void,
408 ];
409 let status = unsafe {
410 cuLaunchKernel(
411 kernel,
412 blocks,
413 1,
414 1,
415 threads,
416 1,
417 1,
418 0,
419 stream,
420 params.as_mut_ptr(),
421 ptr::null_mut(),
422 )
423 };
424 if status != 0 {
425 anyhow::bail!("cuLaunchKernel (bf16_add_inplace) failed: status {status}");
426 }
427 } else {
428 anyhow::bail!("bf16_add_inplace kernel not set — call set_add_kernel() first");
429 }
430
431 Ok(())
432 }
433
434 fn generate_unique_id() -> Result<NcclUniqueId> {
435 let mut id = NcclUniqueId {
436 internal: [0u8; 128],
437 };
438 let result = unsafe { nccl::ncclGetUniqueId(&mut id) };
439 nccl::check_nccl(result, "ncclGetUniqueId")?;
440 Ok(id)
441 }
442
443 fn distribute_id(id: &NcclUniqueId, addr: &str, port: u16, world_size: usize) -> Result<()> {
446 let bind_addr = format!("0.0.0.0:{port}");
447 let listener = TcpListener::bind(&bind_addr)
448 .with_context(|| format!("Rank 0: failed to bind {bind_addr}"))?;
449 tracing::info!(
450 "Rank 0: waiting for {} worker(s) on {}",
451 world_size - 1,
452 bind_addr
453 );
454
455 for i in 0..(world_size - 1) {
456 let (mut stream, peer_addr) = listener.accept().context("Rank 0: accept failed")?;
457 stream
458 .write_all(&id.internal)
459 .context("Rank 0: failed to send unique ID")?;
460 tracing::info!("Rank 0: sent unique ID to worker {} ({})", i + 1, peer_addr);
461 }
462 let _ = addr;
463 Ok(())
464 }
465
466 fn receive_id(addr: &str, port: u16) -> Result<NcclUniqueId> {
469 let target = format!("{addr}:{port}");
470 tracing::info!("Rank N: connecting to master at {target}");
471
472 const MAX_ATTEMPTS: u32 = 600;
476 let mut stream = None;
477 for attempt in 0..MAX_ATTEMPTS {
478 match TcpStream::connect(&target) {
479 Ok(s) => {
480 stream = Some(s);
481 break;
482 }
483 Err(e) => {
484 if attempt + 1 < MAX_ATTEMPTS {
485 if attempt < 30 || attempt.is_multiple_of(30) {
486 tracing::info!(
487 "Connect attempt {}/{MAX_ATTEMPTS}: {e}, retrying in 1s (rank 0 may be loading weights)",
488 attempt + 1
489 );
490 }
491 std::thread::sleep(std::time::Duration::from_secs(1));
492 } else {
493 return Err(e).with_context(|| {
494 format!(
495 "Failed to connect to master at {target} \
496 after {MAX_ATTEMPTS} attempts (~{} minutes)",
497 MAX_ATTEMPTS / 60
498 )
499 });
500 }
501 }
502 }
503 }
504
505 let mut id = NcclUniqueId {
506 internal: [0u8; 128],
507 };
508 stream
509 .unwrap()
510 .read_exact(&mut id.internal)
511 .context("Failed to receive unique ID from rank 0")?;
512 tracing::info!("Received NCCL unique ID from master");
513 Ok(id)
514 }
515}
516
517impl Drop for NcclBackend {
518 fn drop(&mut self) {
519 let comm = *self.comm.lock();
520 let mut handles = self.registered_handles.lock();
521 for handle in handles.drain(..) {
522 unsafe { nccl::ncclCommDeregister(comm, handle) };
523 }
524 drop(handles);
525 if self.recv_buffer != 0 {
526 unsafe { cuMemFree_v2(self.recv_buffer) };
527 }
528 nccl::destroy_event(self.compute_done_event);
529 nccl::destroy_event(self.comm_done_event);
530 nccl::destroy_stream(self.comm_stream);
531 if !comm.is_null() {
532 unsafe { nccl::ncclCommDestroy(comm) };
533 }
534 }
535}
536
537mod comm_impl;