Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions projects/rccl/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ Full documentation for RCCL is available at [https://rccl.readthedocs.io](https:
* Fixed multi-receive `irecv` failing with a fatal network error in the `IB` transport when `NCCL_IB_USE_INLINE=1`.
* Fixed Anvil SDMA (`NCCL_GIN_TYPE=7`) GIN contexts sharing signal cells across logical contexts on a connection, which let a consecutive `ncclGinAllContexts` barrier return early on a stale count. Each logical context now has its own signals.
* Fixed GIN signal shadows being sized for the requested context count instead of the rounded-up `ginContextCount`, which let the last contexts write past the shadow region when the GIN connection count does not divide the requested context count.
* Fixed `ibv_reg_mr_iova2 failed with error Invalid argument` on AMD AINIC when several communicators open P2P network connections without GPU Direct RDMA, for example Direct AllGather with `NCCL_PXN_DISABLE=1`. The host-memory shared P2P pools used 4 KiB pages, so each 256 MiB pool took 65,536 of the NIC's per-LIF page-table entries. They now use 2 MiB pages; `RCCL_NET_SHARED_HOST_HUGEPAGES` (default `1`) set to `0` restores the previous allocation.

### Known issues
* The FP8 ReduceSum and ReduceCopy device APIs are not exported in the LLVM bitcode library.
Expand Down
10 changes: 10 additions & 0 deletions projects/rccl/docs/api-reference/env-variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,16 @@ in the following table.
- | Integer value in bytes (default: ``128``)
| ``N``: Split only when message size >= N bytes

* - | ``RCCL_NET_SHARED_HOST_HUGEPAGES``
| Backs the host-memory shared P2P network staging pools (used by
connections without GPU Direct RDMA) with 2 MiB pages, so each
pool takes fewer NIC memory-registration page-table entries.
Falls back to the regular pinned host allocation if the huge-page
allocation or registration fails. Hosts with transparent huge
pages set to ``never`` get 4 KiB pages either way.
- | ``1``: Use 2 MiB pages (default).
| ``0``: Use the regular pinned host allocation.

* - | ``RCCL_IB_GDR_FLUSH_FLAG``
| Replaces the GPUDirect RDMA flush of Simple-protocol receives in the
``IB`` network transport (``net_ib``). By default, the proxy fences
Expand Down
2 changes: 2 additions & 0 deletions projects/rccl/src/include/proxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ struct ncclProxySharedP2p {
int64_t size;
char* cudaBuff;
char* hostBuff;
char* hostBuffDev; // Device-side address of hostBuff
size_t hostBuffMapSize; // Non-zero when hostBuff is a huge-page mmap registered with cudaHostRegister
// CUDA IPC
ncclIpcDesc ipcDesc;
int dmaBufFd; // DMA-BUF fd for cuMem allocations
Expand Down
125 changes: 122 additions & 3 deletions projects/rccl/src/transport/net.cc
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,125 @@ static ncclResult_t recvFree(struct ncclComm* comm, struct ncclConnector* recv)
}

#define NCCL_SHARED_STEPS 16

#if defined(NCCL_OS_LINUX)
// Host-memory shared P2P pools are registered on every NIC that serves a non-GDR P2P connection.
// With 4 KiB pages a 256 MiB pool takes 65,536 NIC page-table entries; AINIC caps them per LIF
// (rdma_npts_per_lif = 1,048,576), so one communicator's pools nearly fill a NIC and the next
// ibv_reg_mr fails with EINVAL. Backing the pool with transparent huge pages lets the MR use
// 2 MiB pages (128 entries per 256 MiB).
RCCL_PARAM(NetSharedHostHugePages, "NET_SHARED_HOST_HUGEPAGES", 1);
static constexpr size_t rcclHugePageSize = 2UL << 20;

static size_t rcclAnonHugeKb(const void* addr) {
FILE* f = fopen("/proc/self/smaps", "r");
if (f == NULL) return 0;
char line[512];
bool inVma = false;
size_t kb = 0;
while (fgets(line, sizeof(line), f)) {
unsigned long start, end;
if (sscanf(line, "%lx-%lx ", &start, &end) == 2) {
if (inVma) break;
inVma = (uintptr_t)addr >= start && (uintptr_t)addr < end;
} else if (inVma && sscanf(line, "AnonHugePages: %zu kB", &kb) == 1) {
break;
}
}
fclose(f);
return kb;
}

// Backs state->hostBuff with a 2 MiB-aligned anonymous mapping that is registered with the GPU runtime. Returns false
// when the huge-page path is disabled or fails; in that case nothing stays mapped or registered.
static bool sharedHostBuffAllocHuge(struct ncclProxySharedP2p* state) {
if (!rcclParamNetSharedHostHugePages() || (size_t)state->size < rcclHugePageSize) return false;
size_t mapSize = ROUNDUP((size_t)state->size, rcclHugePageSize);
char* raw =
(char*)mmap(NULL, mapSize + rcclHugePageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (raw == MAP_FAILED) {
ATTN("NET/Shared host pool: mmap of %zu bytes failed (%s), using hipHostMalloc", mapSize, strerror(errno));
return false;
}
char* ptr = (char*)ROUNDUP((uintptr_t)raw, rcclHugePageSize);
size_t head = ptr - raw;
if (head) munmap(raw, head);
if (rcclHugePageSize - head) munmap(ptr + mapSize, rcclHugePageSize - head);
if (madvise(ptr, mapSize, MADV_HUGEPAGE) != 0) {
INFO(NCCL_ALLOC | NCCL_NET, "NET/Shared host pool: madvise(MADV_HUGEPAGE) failed: %s", strerror(errno));
}
memset(ptr, 0, mapSize);

// No CUDACHECK between the mapping and the end of this block: an early return would leak the mapping.
void* devPtr = NULL;
bool registered = false;
cudaStreamCaptureMode mode = cudaStreamCaptureModeRelaxed;
cudaError_t err = cudaThreadExchangeStreamCaptureMode(&mode);
if (err == cudaSuccess) {
#if defined(HIP_HOST_UNCACHED_MEMORY)
err = cudaHostRegister(ptr, mapSize,
cudaHostRegisterPortable | cudaHostRegisterMapped | hipExtHostRegisterUncached);
#else
err = cudaHostRegister(ptr, mapSize, cudaHostRegisterPortable | cudaHostRegisterMapped);
#endif
registered = (err == cudaSuccess);
if (registered) err = cudaHostGetDevicePointer(&devPtr, ptr, 0);
// Restore the capture mode whether or not the registration succeeded.
cudaError_t restoreErr = cudaThreadExchangeStreamCaptureMode(&mode);
if (err == cudaSuccess) err = restoreErr;
}
if (err != cudaSuccess) {
ATTN("NET/Shared host pool: huge-page registration failed (%s), using hipHostMalloc", cudaGetErrorString(err));
if (registered) (void)cudaHostUnregister(ptr);
(void)cudaGetLastError();
munmap(ptr, mapSize);
return false;
}
state->hostBuff = ptr;
state->hostBuffDev = (char*)devPtr;
state->hostBuffMapSize = mapSize;
INFO(NCCL_INIT | NCCL_NET, "NET/Shared host pool %p (dev %p) size %zu: %zu kB on huge pages", ptr, devPtr, mapSize,
rcclAnonHugeKb(ptr));
return true;
}

static ncclResult_t sharedHostBuffFreeHuge(struct ncclProxySharedP2p* state) {
// Same rule as ncclCudaHostFree: at process exit the GPU runtime may already be destroyed, so leave the mapping to
// the OS instead of calling into it.
if (rcclShutdownFlag().load(std::memory_order_acquire)) {
INFO(NCCL_ALLOC, "NET/Shared host pool: skipping free (process shutdown) pointer %p", state->hostBuff);
return ncclSuccess;
}
CUDACHECK(cudaHostUnregister(state->hostBuff));
munmap(state->hostBuff, state->hostBuffMapSize);
return ncclSuccess;
}
#else
// Huge-page backing is Linux only; other platforms use hipHostMalloc.
static bool sharedHostBuffAllocHuge(struct ncclProxySharedP2p*) { return false; }
static ncclResult_t sharedHostBuffFreeHuge(struct ncclProxySharedP2p*) { return ncclSuccess; }
#endif // NCCL_OS_LINUX

static ncclResult_t sharedHostBuffAlloc(struct ncclProxySharedP2p* state) {
state->hostBuffMapSize = 0;
if (sharedHostBuffAllocHuge(state)) return ncclSuccess;
NCCLCHECK(ncclCudaHostCalloc(&state->hostBuff, state->size));
state->hostBuffDev = state->hostBuff;
return ncclSuccess;
}

static ncclResult_t sharedHostBuffFree(struct ncclProxySharedP2p* state) {
if (state->hostBuffMapSize) {
NCCLCHECK(sharedHostBuffFreeHuge(state));
} else {
NCCLCHECK(ncclCudaHostFree(state->hostBuff));
}
state->hostBuff = NULL;
state->hostBuffDev = NULL;
state->hostBuffMapSize = 0;
return ncclSuccess;
}

static ncclResult_t sharedNetBuffersInit(struct ncclProxyState* proxyState, int cuda, int tpLocalRank, int type,
int sameProcess, int nChannels, char** gpuPtr, char** cpuPtr, ssize_t* size,
ncclIpcDesc* ipcDesc) {
Expand Down Expand Up @@ -929,10 +1048,10 @@ static ncclResult_t sharedNetBuffersInit(struct ncclProxyState* proxyState, int
}
}
if (!cuda && state->hostBuff == NULL) {
NCCLCHECK(ncclCudaHostCalloc(&state->hostBuff, state->size));
NCCLCHECK(sharedHostBuffAlloc(state));
}
if (cpuPtr) *cpuPtr = cuda ? state->cudaBuff : state->hostBuff;
if (gpuPtr) *gpuPtr = (cpuPtr && sameProcess) ? *cpuPtr : NULL;
if (gpuPtr) *gpuPtr = (cpuPtr && sameProcess) ? (cuda ? state->cudaBuff : state->hostBuffDev) : NULL;
if (ipcDesc) memcpy(ipcDesc, &state->ipcDesc, sizeof(state->ipcDesc));
return ncclSuccess;
}
Expand Down Expand Up @@ -964,7 +1083,7 @@ static ncclResult_t sharedNetBuffersDestroy(struct ncclProxyState* proxyState, i
}
NCCLCHECK(ncclCudaFree(state->cudaBuff, proxyState->memManager));
}
if (state->hostBuff) NCCLCHECK(ncclCudaHostFree(state->hostBuff));
if (state->hostBuff) NCCLCHECK(sharedHostBuffFree(state));
}

if (peer->send.refcount || peer->recv.refcount) return ncclSuccess;
Expand Down
25 changes: 24 additions & 1 deletion projects/rccl/test/host/fakes/hip_fakes.cc
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@ static hipError_t DefaultHipHostFree(void* ptr)
}
std::function<hipError_t(void*)> g_hipHostFree = DefaultHipHostFree;

static hipError_t DefaultHipHostRegister(void*, std::size_t, unsigned) { return hipErrorInvalidValue; }
std::function<hipError_t(void*, std::size_t, unsigned)> g_hipHostRegister = DefaultHipHostRegister;

static hipError_t DefaultHipHostUnregister(void*) { return hipSuccess; }
std::function<hipError_t(void*)> g_hipHostUnregister = DefaultHipHostUnregister;

// Same as the plain stub it replaces: fails without touching *devPtr.
static hipError_t DefaultHipHostGetDevicePointer(void**, void*, unsigned) { return hipErrorInvalidValue; }
std::function<hipError_t(void**, void*, unsigned)> g_hipHostGetDevicePointer = DefaultHipHostGetDevicePointer;

// --- device inventory + current-device state ----------------------------
int g_deviceCount = 8;
int g_currentDevice = 0;
Expand Down Expand Up @@ -658,6 +668,9 @@ void ResetHipFakes()
g_hipMallocManaged = DefaultHipMallocManaged;
g_hipFree = DefaultHipFree;
g_hipHostFree = DefaultHipHostFree;
g_hipHostRegister = DefaultHipHostRegister;
g_hipHostUnregister = DefaultHipHostUnregister;
g_hipHostGetDevicePointer = DefaultHipHostGetDevicePointer;
g_hipGetDevice = DefaultHipGetDevice;
g_hipSetDevice = DefaultHipSetDevice;
g_hipGetDeviceCount = DefaultHipGetDeviceCount;
Expand Down Expand Up @@ -885,6 +898,13 @@ hipError_t hipHostMalloc(void** ptr, size_t size, unsigned int flags)
return g_hipHostMalloc(ptr, size, flags);
}

hipError_t hipHostRegister(void* ptr, size_t size, unsigned int flags)
{
return g_hipHostRegister(ptr, size, flags);
}

hipError_t hipHostUnregister(void* ptr) { return g_hipHostUnregister(ptr); }

hipError_t hipIpcCloseMemHandle(void* ptr) { return g_hipIpcCloseMemHandle(ptr); }

hipError_t hipIpcOpenMemHandle(void** devPtr, hipIpcMemHandle_t handle, unsigned int flags)
Expand Down Expand Up @@ -1069,7 +1089,10 @@ hipError_t hipStreamCreate(hipStream_t*) { return hipErrorInvalidValue; }
// hipStreamCreateWithPriority / hipDeviceGetStreamPriorityRange are defined
// above (seam-routed) -- the develop merge added plainer duplicates here.
hipError_t hipPointerGetAttributes(hipPointerAttribute_t*, const void*) { return hipErrorInvalidValue; }
hipError_t hipHostGetDevicePointer(void**, void*, unsigned int) { return hipErrorInvalidValue; }
hipError_t hipHostGetDevicePointer(void** devPtr, void* hostPtr, unsigned int flags)
{
return g_hipHostGetDevicePointer(devPtr, hostPtr, flags);
}
hipError_t hipIpcGetEventHandle(hipIpcEventHandle_t*, hipEvent_t) { return hipErrorInvalidValue; }
hipError_t hipEventSynchronize(hipEvent_t) { return hipErrorInvalidValue; }

Expand Down
5 changes: 5 additions & 0 deletions projects/rccl/test/host/fakes/hip_fakes.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ extern std::function<hipError_t(void** /*ptr*/, std::size_t /*size*/, unsigned /
g_hipMallocManaged;
extern std::function<hipError_t(void* /*ptr*/)> g_hipFree;
extern std::function<hipError_t(void* /*ptr*/)> g_hipHostFree;
// Pinning of caller-owned host memory. Register and GetDevicePointer default to hipErrorInvalidValue, the fail-loud
// floor, so an unhooked caller takes its hipHostMalloc fallback. Unregister defaults to hipSuccess.
extern std::function<hipError_t(void* /*hostPtr*/, std::size_t /*size*/, unsigned /*flags*/)> g_hipHostRegister;
extern std::function<hipError_t(void* /*hostPtr*/)> g_hipHostUnregister;
extern std::function<hipError_t(void** /*devPtr*/, void* /*hostPtr*/, unsigned /*flags*/)> g_hipHostGetDevicePointer;
extern int g_deviceCount;
extern int g_currentDevice;
extern std::function<hipError_t(int* /*dev*/)> g_hipGetDevice;
Expand Down
Loading
Loading