use super::*;
use crate::engine::partitioned::in_process::InProcessTransport;
#[test]
fn a_probe_that_never_readies_times_out() {
let start = Instant::now();
let mut calls = 0;
let ready = poll_until(Duration::from_millis(20), || {
calls += 1;
Ok(false)
})
.expect("the probe never fails");
assert!(!ready);
assert!(calls > 1);
assert!(start.elapsed() < Duration::from_secs(5));
}
#[test]
fn a_probe_error_ends_the_poll() {
let err = poll_until(Duration::from_secs(60), || Err(GpuError::Unsupported("probe")))
.expect_err("the error propagates");
assert!(matches!(err, GpuError::Unsupported("probe")));
}
#[test]
fn the_unique_id_survives_the_word_packing() {
let mut id = sys::ncclUniqueId { internal: [0; 128] };
for (i, c) in id.internal.iter_mut().enumerate() {
*c = (i as u8).wrapping_mul(37).wrapping_add(11) as std::ffi::c_char;
}
let mut words = [0u64; UNIQUE_ID_WORDS];
pack_id(&id, &mut words);
assert_eq!(unpack_id(&words), id);
}
#[test]
fn the_id_broadcast_reaches_every_rank_over_an_all_reduce() {
let mut id = sys::ncclUniqueId { internal: [0; 128] };
for (i, c) in id.internal.iter_mut().enumerate() {
*c = (255 - i as u8) as std::ffi::c_char;
}
let got: Vec<sys::ncclUniqueId> = std::thread::scope(|s| {
let handles: Vec<_> = InProcessTransport::group(4)
.into_iter()
.map(|t| {
s.spawn(move || {
let mut buf = [0u64; UNIQUE_ID_WORDS + 1];
if t.rank() == 0 {
pack_id(&id, &mut buf[..UNIQUE_ID_WORDS]);
}
t.allreduce_sum_u64(&mut buf);
assert_eq!(buf[UNIQUE_ID_WORDS], 0);
unpack_id(&buf[..UNIQUE_ID_WORDS])
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
assert!(got.iter().all(|g| *g == id));
}
#[test]
fn the_start_is_agreed_for_every_readiness_and_device() {
let run = |ranks: &[(bool, [u64; 2])]| -> Vec<Result<(), String>> {
let size = ranks.len() as u32;
std::thread::scope(|s| {
let hs: Vec<_> = InProcessTransport::group(size)
.into_iter()
.map(|t| {
let (can, id) = ranks[t.rank() as usize];
s.spawn(move || agree_start(&t, can, id).map_err(|e| e.to_string()))
})
.collect();
hs.into_iter().map(|h| h.join().unwrap()).collect()
})
};
let mut cases: Vec<Vec<(bool, [u64; 2])>> = Vec::new();
for c0 in [false, true] {
for c1 in [false, true] {
for id1 in [[1, 0], [2, 0]] {
cases.push(vec![(c0, [1, 0]), (c1, id1)]);
}
}
}
for mask in 0..32u32 {
cases.push(
(0..4)
.map(|r| {
let id = if mask & 16 != 0 && r == 3 { 1 } else { r as u64 + 1 };
((mask >> r) & 1 == 0, [id, 9])
})
.collect(),
);
}
for ranks in &cases {
let got = run(ranks);
assert!(got.iter().all(|g| g == &got[0]), "{ranks:?}: ranks disagree: {got:?}");
let ids: Vec<_> = ranks.iter().map(|r| r.1).collect();
let shared = (0..ids.len()).any(|i| ids[i + 1..].contains(&ids[i]));
let can = ranks.iter().all(|r| r.0);
match &got[0] {
Ok(()) => assert!(can && !shared, "{ranks:?}"),
Err(msg) if !can => assert!(msg.contains("libnccl"), "{msg}"),
Err(msg) => assert!(shared && msg.contains("share a device"), "{msg}"),
}
}
}
#[test]
fn the_config_matches_the_initializer_layout() {
let c = default_config();
assert_eq!(c.size, 48);
assert_eq!(c.magic, 0xcafe_beef);
assert_eq!(c.blocking, i32::MIN);
assert!(c.netName.is_null());
}
static REAL_NCCL: Mutex<()> = Mutex::new(());
type OneRank = (MutexGuard<'static, ()>, NcclComm, Arc<CudaStream>);
fn one_rank() -> Option<OneRank> {
if !crate::engine::gpu::nccl_available() {
return None;
}
let guard = REAL_NCCL.lock().unwrap_or_else(PoisonError::into_inner);
let context = super::super::device::context(0).expect("a visible device");
let t = InProcessTransport::group(1).pop().expect("one rank");
let comm = NcclComm::init(&t, &context)
.unwrap_or_else(|e| panic!("a one-rank communicator fails to initialize: {e}"));
comm.set_timeout(Duration::from_secs(60));
let stream = context.new_stream().expect("a stream");
Some((guard, comm, stream))
}
#[test]
fn a_one_rank_communicator_warms_up_and_shuts_down_cleanly() {
let Some((_nccl, comm, stream)) = one_rank() else {
return;
};
assert_eq!((comm.rank, comm.size), (0, 1));
comm.warm_up(&stream).expect("the warm-up completes");
assert!(comm.is_healthy());
drop(comm);
assert!(
ABORTS.lock().unwrap().is_empty(),
"a healthy communicator shuts down without an abort"
);
}
#[test]
fn an_aborted_communicator_refuses_every_later_call() {
let Some((_nccl, comm, stream)) = one_rank() else {
return;
};
comm.abort();
comm.abort();
assert!(!comm.is_healthy());
assert!(matches!(comm.warm_up(&stream), Err(GpuError::Nccl { .. })));
drop(comm);
assert!(ABORTS.lock().unwrap().is_empty(), "a test build joins every abort thread at drop");
}
const SIZES: [usize; 7] = [1, 1000, 3, 65_536 + 5, 0, 17, 257 * 1024];
fn messages() -> Vec<Vec<u64>> {
SIZES
.iter()
.enumerate()
.map(|(m, &n)| {
(0..n as u64)
.map(|i| ((m as u64 + 1) << 48) ^ i.wrapping_mul(0x9E37_79B9_7F4A_7C15))
.collect()
})
.collect()
}
fn upload(stream: &Arc<CudaStream>, msgs: &[Vec<u64>]) -> Vec<cudarc::driver::CudaSlice<u64>> {
msgs.iter()
.map(|m| {
let mut s = stream.alloc_zeros::<u64>(m.len().max(1)).expect("alloc");
if !m.is_empty() {
stream.memcpy_htod(m, &mut s).expect("upload");
}
s
})
.collect()
}
#[test]
fn interleaved_self_sends_match_in_posting_order() {
let Some((_nccl, comm, stream)) = one_rank() else {
return;
};
let wire = comm;
assert_eq!((wire.rank(), wire.size()), (0, 1));
let msgs = messages();
let sends = upload(&stream, &msgs);
let mut receives: Vec<_> =
SIZES.iter().map(|&n| stream.alloc_zeros::<u64>(n + 1).expect("alloc")).collect();
let mut group = WireGroup::new();
for ((send, received), &n) in sends.iter().zip(receives.iter_mut()).zip(&SIZES) {
group.send(send.slice(0..n), 0, &stream);
group.receive(received.slice_mut(0..n), 0, &stream);
}
assert_eq!(group.ops().len(), 2 * SIZES.len());
group.post(&wire).expect("the group posts");
wire.wait(&stream).expect("the group completes");
for (m, (msg, received)) in msgs.iter().zip(&receives).enumerate() {
let got = stream.clone_dtoh(received).expect("download");
assert_eq!(&got[..msg.len()], &msg[..], "message {m} of {} u64", msg.len());
assert_eq!(got[msg.len()], 0, "message {m} overran its receive");
}
assert!(wire.is_healthy());
}
#[test]
fn self_sends_land_in_one_column_in_posting_order() {
let Some((_nccl, comm, stream)) = one_rank() else {
return;
};
let wire = comm;
let msgs = messages();
let sends = upload(&stream, &msgs);
let total: usize = SIZES.iter().sum();
let mut received = stream.alloc_zeros::<u64>(total + 1).expect("alloc");
let mut group = WireGroup::new();
for (send, &n) in sends.iter().zip(&SIZES) {
group.send(send.slice(0..n), 0, &stream);
}
let parts: Vec<(usize, u32)> = SIZES.iter().map(|&n| (n, 0)).collect();
group.receive_parts(received.as_view_mut(), &parts, &stream);
group.post(&wire).expect("the group posts");
wire.wait(&stream).expect("the group completes");
let got = stream.clone_dtoh(&received).expect("download");
let mut at = 0;
for (m, msg) in msgs.iter().enumerate() {
assert_eq!(&got[at..at + msg.len()], &msg[..], "message {m} of {} u64", msg.len());
at += msg.len();
}
assert_eq!(got[total], 0, "nothing lands past the last message");
assert!(wire.is_healthy());
}
#[test]
fn an_unmatched_self_receive_fails_the_group_and_aborts() {
let Some((_nccl, comm, stream)) = one_rank() else {
return;
};
let wire = comm;
let mut destination = stream.alloc_zeros::<u64>(4).expect("alloc");
let mut group = WireGroup::new();
group.receive(destination.as_view_mut(), 0, &stream);
let posted = group.post(&wire);
assert!(matches!(posted, Err(GpuError::Nccl { code: 5, .. })), "{posted:?}");
assert!(!wire.is_healthy());
assert!(matches!(wire.wait(&stream), Err(GpuError::Nccl { .. })));
}
#[test]
fn a_forced_timeout_returns_within_the_bound_and_aborts() {
let Some((_nccl, comm, stream)) = one_rank() else {
return;
};
let bound = Duration::from_millis(300);
comm.set_timeout(bound);
comm.inject(WireFault::Wait);
let wire = comm;
let source = stream.alloc_zeros::<u64>(64).expect("alloc");
let mut destination = stream.alloc_zeros::<u64>(64).expect("alloc");
let mut group = WireGroup::new();
group.send(source.as_view(), 0, &stream);
group.receive(destination.as_view_mut(), 0, &stream);
group.post(&wire).expect("the group posts");
let start = Instant::now();
let waited = wire.wait(&stream);
let elapsed = start.elapsed();
assert!(matches!(waited, Err(GpuError::Timeout { .. })), "{waited:?}");
assert!(elapsed >= bound, "{elapsed:?}");
assert!(elapsed < Duration::from_secs(30), "{elapsed:?}");
assert!(!wire.is_healthy());
assert!(matches!(wire.wait(&stream), Err(GpuError::Nccl { .. })));
stream.synchronize().expect("the stream drains");
drop(wire);
}
#[test]
fn an_injected_post_failure_aborts_the_communicator() {
let Some((_nccl, comm, stream)) = one_rank() else {
return;
};
comm.inject(WireFault::Post);
let source = stream.alloc_zeros::<u64>(8).expect("alloc");
let mut destination = stream.alloc_zeros::<u64>(8).expect("alloc");
let mut group = WireGroup::new();
group.send(source.as_view(), 0, &stream);
group.receive(destination.as_view_mut(), 0, &stream);
assert!(matches!(group.post(&comm), Err(GpuError::Wire(m)) if m.contains("injected")));
assert!(!comm.is_healthy());
}
#[test]
fn an_op_naming_a_peer_outside_the_group_is_refused_before_nccl() {
let Some((_nccl, comm, stream)) = one_rank() else {
return;
};
let wire = comm;
let source = stream.alloc_zeros::<u8>(8).expect("alloc");
let mut group = WireGroup::new();
group.send(source.as_view(), 1, &stream);
assert!(matches!(group.post(&wire), Err(GpuError::Nccl { .. })));
assert!(wire.is_healthy());
}
impl NcclComm {
pub(crate) fn set_timeout(&self, timeout: Duration) {
self.lock().timeout = timeout;
}
}