distributed: NCCL P2P primitives (PpContext + send/recv)

Add ncclSend/ncclRecv FFI and a PpContext that initializes a NCCL
communicator across P pipeline stages and hands the hidden state to
neighbour stages on the null stream. Mirrors TpContext; the collective
differs (point-to-point hand-off vs in-layer AllReduce).

tests/sendrecv.rs: 2-GPU stage0->stage1 send/recv smoke test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 18:45:42 +08:00
parent c2362df1f1
commit 859c0cc0b6
3 changed files with 143 additions and 0 deletions

View File

@@ -45,6 +45,23 @@ unsafe extern "C" {
comm: NcclComm,
stream: CudaStream,
) -> i32;
// Point-to-point primitives for pipeline parallelism (Phase 18).
pub fn ncclSend(
sendbuff: *const c_void,
count: usize,
datatype: i32,
peer: i32,
comm: NcclComm,
stream: CudaStream,
) -> i32;
pub fn ncclRecv(
recvbuff: *mut c_void,
count: usize,
datatype: i32,
peer: i32,
comm: NcclComm,
stream: CudaStream,
) -> i32;
pub fn ncclGroupStart() -> i32;
pub fn ncclGroupEnd() -> i32;
pub fn ncclGetErrorString(result: i32) -> *const c_char;

View File

@@ -95,3 +95,67 @@ impl Drop for TpContext {
}
}
}
/// Per-stage pipeline-parallel context: a NCCL communicator spanning all `P`
/// stages plus point-to-point send/recv of the hidden state to the neighbour
/// stages. Init is identical to `TpContext` (one comm across `world` ranks);
/// only the collective differs — PP hands off `[tokens, hidden]` between
/// consecutive stages instead of AllReducing within a layer.
pub struct PpContext {
pub stage: usize,
pub world: usize,
pub device: u32,
comm: NcclComm,
}
// The NCCL communicator is owned by exactly one stage thread.
unsafe impl Send for PpContext {}
impl PpContext {
/// Initialize this stage. Must be called from the thread that owns this
/// stage's GPU; binds the thread to `device` first. All stages call this
/// concurrently with the same `id` and `world`.
pub fn init(stage: usize, world: usize, id: NcclUniqueId, device: u32) -> Self {
device::set_device(device).expect("set_device");
let mut comm: NcclComm = std::ptr::null_mut();
ffi::check(unsafe { ffi::ncclGroupStart() }, "ncclGroupStart(init)");
ffi::check(
unsafe { ffi::ncclCommInitRank(&mut comm, world as i32, id, stage as i32) },
"ncclCommInitRank",
);
ffi::check(unsafe { ffi::ncclGroupEnd() }, "ncclGroupEnd(init)");
Self { stage, world, device, comm }
}
/// Send `count` BF16 elements at `ptr` to `peer`, on the null stream so it is
/// ordered after the producing matmul. Asynchronous — a later `synchronize`
/// (the caller must do one before reusing/freeing the buffer) completes it.
///
/// # Safety
/// `ptr` must point to at least `count` BF16 elements of valid device memory.
pub fn send_bf16_ptr(&self, ptr: *const c_void, count: usize, peer: usize) {
ffi::check(
unsafe { ffi::ncclSend(ptr, count, ffi::NCCL_BF16, peer as i32, self.comm, NULL_STREAM) },
"ncclSend",
);
}
/// Receive `count` BF16 elements from `peer` into `ptr`, on the null stream.
///
/// # Safety
/// `ptr` must point to at least `count` BF16 elements of valid device memory.
pub fn recv_bf16_ptr(&self, ptr: *mut c_void, count: usize, peer: usize) {
ffi::check(
unsafe { ffi::ncclRecv(ptr, count, ffi::NCCL_BF16, peer as i32, self.comm, NULL_STREAM) },
"ncclRecv",
);
}
}
impl Drop for PpContext {
fn drop(&mut self) {
if !self.comm.is_null() {
unsafe { ffi::ncclCommDestroy(self.comm) };
}
}
}