Merge remote-tracking branch 'nccl/master' into develop
This commit is contained in:
+214
-89
@@ -84,7 +84,6 @@ NCCL_PARAM(L1SharedMemoryCarveout, "L1_SHARED_MEMORY_CARVEOUT", 0);
|
||||
ncclResult_t ncclInitKernelsForDevice(int cudaArch, int maxSharedMem, size_t* maxStackSize) {
|
||||
constexpr int KernelCount = sizeof(rcclKernelTable)/sizeof(rcclKernelTable[0]);
|
||||
ncclResult_t result = ncclSuccess;
|
||||
int print = 0;
|
||||
|
||||
if (maxStackSize) *maxStackSize = 0;
|
||||
int carveout = ncclParamL1SharedMemoryCarveout();
|
||||
@@ -115,11 +114,9 @@ ncclResult_t ncclInitKernelsForDevice(int cudaArch, int maxSharedMem, size_t* ma
|
||||
if (ncclMaxSharedMem != 0) {
|
||||
int sharedMemSize = ncclMaxSharedMem;
|
||||
if (sharedMemSize > (maxSharedMem-attr.sharedSizeBytes)) {
|
||||
if (print++ == 0)
|
||||
INFO(NCCL_INIT, "ncclMaxSharedMem %d exceeds device/fn maxSharedMem %zu",
|
||||
sharedMemSize, maxSharedMem-attr.sharedSizeBytes);
|
||||
// Reduce requested MaxDynamicSharedMemorySize attribute
|
||||
sharedMemSize = maxSharedMem - attr.sharedSizeBytes;
|
||||
WARN("cudaArch %d ncclMaxSharedMem %d exceeds device/fn maxSharedMem %zu",
|
||||
cudaArch, sharedMemSize, maxSharedMem-attr.sharedSizeBytes);
|
||||
return ncclSystemError;
|
||||
}
|
||||
CUDACHECKGOTO(cudaFuncSetAttribute(fn,
|
||||
cudaFuncAttributeMaxDynamicSharedMemorySize, sharedMemSize),
|
||||
@@ -366,6 +363,7 @@ ncclResult_t ncclTasksRegAndEnqueue(struct ncclComm* comm) {
|
||||
devWork.rcclUseOneSlice = comm->rcclUseOneSlice;
|
||||
devWork.isOneRPN = comm->isOneRPN;
|
||||
devWork.netRegUsed = devWork.regUsed = 0;
|
||||
devWork.profilerEnabled = ncclProfilerPluginLoaded() && (task->eActivationMask & ncclProfileKernelCh);
|
||||
if (task->regBufType & NCCL_NET_REG_BUFFER)
|
||||
devWork.netRegUsed = 1;
|
||||
if (task->regBufType & (NCCL_IPC_REG_BUFFER | NCCL_NVLS_REG_BUFFER))
|
||||
@@ -467,6 +465,7 @@ ncclResult_t ncclPrepareTasks(struct ncclComm* comm, bool* algoNeedConnect, bool
|
||||
struct ncclTaskColl* next = aggBeg->next;
|
||||
aggBeg->algorithm = agg.algorithm;
|
||||
aggBeg->protocol = agg.protocol;
|
||||
if (aggBeg->protocol == NCCL_PROTO_LL) aggBeg->trafficBytes *= 4;
|
||||
aggBeg->nMaxChannels = agg.nMaxChannels;
|
||||
aggBeg->nWarps = agg.nWarps;
|
||||
aggBeg->devFuncId = agg.devFuncId;
|
||||
@@ -526,6 +525,7 @@ ncclResult_t ncclPrepareTasks(struct ncclComm* comm, bool* algoNeedConnect, bool
|
||||
devWork.redOpArgIsPtr = task->opDev.scalarArgIsPtr;
|
||||
devWork.oneNode = (comm->nNodes == 1);
|
||||
devWork.netRegUsed = devWork.regUsed = 0;
|
||||
devWork.profilerEnabled = ncclProfilerPluginLoaded() && (task->eActivationMask & ncclProfileKernelCh);
|
||||
if (task->regBufType & NCCL_NET_REG_BUFFER)
|
||||
devWork.netRegUsed = 1;
|
||||
if (task->regBufType & (NCCL_IPC_REG_BUFFER | NCCL_NVLS_REG_BUFFER))
|
||||
@@ -559,6 +559,14 @@ ncclResult_t ncclPrepareTasks(struct ncclComm* comm, bool* algoNeedConnect, bool
|
||||
return ncclSuccess;
|
||||
}
|
||||
|
||||
static ncclResult_t addProfilerProxyOpIfNeeded(struct ncclComm* comm, struct ncclKernelPlan* plan, struct ncclProxyOp* op) {
|
||||
int tmp = op->pattern;
|
||||
op->pattern = ncclPatternProfiler;
|
||||
ncclResult_t ret = addProxyOpIfNeeded(comm, plan, op);
|
||||
op->pattern = tmp;
|
||||
return ret;
|
||||
}
|
||||
|
||||
RCCL_PARAM(IntraNetThreshold, "INTRANET_THRESHOLD", 8388608);
|
||||
|
||||
static ncclResult_t scheduleCollTasksToPlan(
|
||||
@@ -571,7 +579,7 @@ static ncclResult_t scheduleCollTasksToPlan(
|
||||
int nChannels[2*2] = {0, 0, 0, 0}; // [collnet][nvls]
|
||||
int const nMaxChannels[2*2] = {comm->nChannels, comm->nvlsChannels, // [collnet][nvls]
|
||||
comm->nChannels, comm->nvlsChannels};
|
||||
constexpr size_t MinTrafficPerChannel = 512; // Traffic as minimal
|
||||
constexpr size_t MinTrafficPerChannel = 16 << 10; // 16K traffic as minimal
|
||||
do {
|
||||
size_t workBytes = 0;
|
||||
struct ncclTaskColl* task = ncclIntruQueueHead(&planner->collTaskQueue);
|
||||
@@ -634,11 +642,16 @@ static ncclResult_t scheduleCollTasksToPlan(
|
||||
proxyOp.opCount = proxyOpId;
|
||||
proxyOp.task.coll = task;
|
||||
proxyOp.rank = comm->rank;
|
||||
proxyOp.eActivationMask = task->eActivationMask;
|
||||
proxyOp.incWorkCounter = true;
|
||||
addWorkBatchToPlan(comm, plan, c, workNode->workType, task->devFuncId, plan->workBytes);
|
||||
// Set pattern to profiler to add a proxy profiler for kernel events
|
||||
NCCLCHECK(addProxyOpIfNeeded(comm, plan, &proxyOp));
|
||||
NCCLCHECK(addProfilerProxyOpIfNeeded(comm, plan, &proxyOp));
|
||||
}
|
||||
} else { // not task->isCollnet
|
||||
int trafficPerByte = ncclFuncTrafficPerByte(task->func, comm->nRanks);
|
||||
if (task->protocol == NCCL_PROTO_LL) trafficPerByte *= 4;
|
||||
size_t cellSize = divUp(divUp(MinTrafficPerChannel, (size_t)trafficPerByte), 16) * 16;
|
||||
int elementsPerCell = cellSize/elementSize;
|
||||
size_t cells = divUp(task->count*elementSize, cellSize);
|
||||
@@ -762,6 +775,8 @@ static ncclResult_t scheduleCollTasksToPlan(
|
||||
}
|
||||
proxyOp->ringAlgo->incRefCount();
|
||||
}
|
||||
proxyOp->eActivationMask = task->eActivationMask;
|
||||
proxyOp->incWorkCounter = true;
|
||||
proxyOp->connIndex = 0;
|
||||
if (task->protocol == NCCL_PROTO_SIMPLE && task->algorithm == NCCL_ALGO_RING) {
|
||||
if (comm->useIntraNet && nBytes > rcclParamIntraNetThreshold()) {
|
||||
@@ -773,6 +788,7 @@ static ncclResult_t scheduleCollTasksToPlan(
|
||||
// determine if that's actually true but it's also not clear if that would be an issue.
|
||||
// coverity[uninit_use_in_call:FALSE]
|
||||
NCCLCHECK(addProxyOpIfNeeded(comm, plan, proxyOp));
|
||||
NCCLCHECK(addProfilerProxyOpIfNeeded(comm, plan, proxyOp));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -915,7 +931,8 @@ static ncclResult_t addP2pToPlan(
|
||||
if (protocol[dir] == NCCL_PROTO_LL) chunkSize[dir] *= 2;
|
||||
|
||||
if (network[dir]) {
|
||||
if (bytes[dir] > 0 && proxySameProcess[dir] && protocol[dir] == NCCL_PROTO_SIMPLE && (ncclPxnDisable(comm) || !comm->isAllNvlink)) {
|
||||
bool pxnUsed = !ncclPxnDisable(comm) && comm->isAllNvlink && comm->maxLocalRanks > 1;
|
||||
if (bytes[dir] > 0 && proxySameProcess[dir] && protocol[dir] == NCCL_PROTO_SIMPLE && (!pxnUsed)) {
|
||||
int regFlag = 0;
|
||||
NCCLCHECK(ncclCalloc(&handles[dir], nChannelsMax));
|
||||
for (int part = 0; part < nChannelsMax; part++) {
|
||||
@@ -991,6 +1008,7 @@ static ncclResult_t addP2pToPlan(
|
||||
work->recvRank = recvRank;
|
||||
work->recvAddr = recvAddr;
|
||||
work->recvBytes = recvBytes==-1 ? 0 : recvBytes;
|
||||
work->profilerEnabled = ncclProfilerPluginLoaded() && ((p2pTasks[0] ? p2pTasks[0] : p2pTasks[1])->eActivationMask & ncclProfileKernelCh);
|
||||
work->recvConnIndex = connIndex[0];
|
||||
work->recvOpCount = recvOpCount;
|
||||
|
||||
@@ -1010,6 +1028,7 @@ static ncclResult_t addP2pToPlan(
|
||||
op->coll = p2pTasks[dir] ? p2pTasks[dir]->func : 0;
|
||||
op->task.p2p = p2pTasks[dir];
|
||||
op->rank = comm->rank;
|
||||
op->eActivationMask = p2pTasks[dir] ? p2pTasks[dir]->eActivationMask : 0;
|
||||
op->connIndex = connIndex[dir];
|
||||
// The following are modified per channel part in addWorkToChannels():
|
||||
// op->buffer, op->nbytes, op->nsteps = ...;
|
||||
@@ -1017,6 +1036,7 @@ static ncclResult_t addP2pToPlan(
|
||||
|
||||
nChannelsMax = std::max(nChannels[0], nChannels[1]);
|
||||
for (int part=0; part < nChannelsMax; part++) {
|
||||
int incWorkCounter = -1;
|
||||
int channelId = ncclP2pChannelForPart(comm->p2pnChannels, base, part, comm->p2pnChannelsPerPeer, comm->nNodes);
|
||||
plan->channelMask.masks[channelId/64] |= uint64_t(1)<<(channelId%64);
|
||||
// Add batch first.
|
||||
@@ -1058,12 +1078,19 @@ static ncclResult_t addP2pToPlan(
|
||||
}
|
||||
}
|
||||
|
||||
// Increment work counter for <send, recv> pair rather than individual p2p
|
||||
if (proxyOps[dir].nsteps && incWorkCounter < 0) {
|
||||
proxyOps[dir].incWorkCounter = true;
|
||||
incWorkCounter = dir;
|
||||
}
|
||||
|
||||
if (proxyOps[dir].nsteps != 0) {
|
||||
// Calculate the opCount after adding batch since then the batch count will
|
||||
// equal one plus the batch index this p2p settled in.
|
||||
proxyOps[dir].channelId = channelId;
|
||||
proxyOps[dir].opCount = uint64_t(comm->planner.wipPlan.channels[channelId].nWorkBatchesP2p)<<1 | 1;
|
||||
NCCLCHECK(addProxyOpIfNeeded(comm, plan, &proxyOps[dir]));
|
||||
NCCLCHECK(addProfilerProxyOpIfNeeded(comm, plan, &proxyOps[dir]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1289,22 +1316,23 @@ static ncclResult_t uploadWork(struct ncclComm* comm, struct ncclKernelPlan* pla
|
||||
struct uploadWork_cleanup_t* cleanup = nullptr;
|
||||
cudaStreamCaptureMode mode = cudaStreamCaptureModeRelaxed;
|
||||
void* fifoBufDev = nullptr;
|
||||
cudaStream_t deviceStream;
|
||||
|
||||
CUDACHECKGOTO(cudaThreadExchangeStreamCaptureMode(&mode), result, fail);
|
||||
|
||||
// Acquire deviceStream to gain access to deviceStream.cudaStream. Since the
|
||||
// user's graph will be launched later, and it also acquires the deviceStream,
|
||||
// it will observe this upload.
|
||||
NCCLCHECKGOTO(ncclStrongStreamAcquireUncaptured(&comm->sharedRes->deviceStream), result, fail);
|
||||
// Acquire deviceStream. Since the user's graph will be launched later and it also
|
||||
// acquires the deviceStream, it will observe this upload.
|
||||
NCCLCHECKGOTO(ncclStrongStreamAcquire(ncclCudaGraphNone(), &comm->sharedRes->deviceStream, /*concurrent=*/false, &deviceStream), result, fail);
|
||||
|
||||
CUDACHECKGOTO(cudaMallocAsync(&fifoBufDev, workBytes, comm->memPool, comm->sharedRes->deviceStream.cudaStream), result, fail);
|
||||
CUDACHECKGOTO(cudaMallocAsync(&fifoBufDev, workBytes, comm->memPool, deviceStream), result, fail);
|
||||
plan->workBufPersistent = fifoBufDev;
|
||||
plan->kernelArgs->workBuf = fifoBufDev;
|
||||
|
||||
// coverity[uninit_use_in_call:FALSE] => fifoBufHost is never NULL
|
||||
CUDACHECKGOTO(cudaMemcpyAsync(fifoBufDev, fifoBufHost, workBytes, cudaMemcpyDefault, comm->sharedRes->deviceStream.cudaStream), result, fail);
|
||||
CUDACHECKGOTO(cudaMemcpyAsync(fifoBufDev, fifoBufHost, workBytes, cudaMemcpyDefault, deviceStream), result, fail);
|
||||
cudaEvent_t memcpyDone;
|
||||
CUDACHECKGOTO(cudaEventCreateWithFlags(&memcpyDone, cudaEventDisableTiming), result, fail);
|
||||
CUDACHECKGOTO(cudaEventRecord(memcpyDone, comm->sharedRes->deviceStream.cudaStream), result, fail);
|
||||
CUDACHECKGOTO(cudaEventRecord(memcpyDone, deviceStream), result, fail);
|
||||
|
||||
NCCLCHECKGOTO(ncclCalloc(&cleanup, 1), result, fail);
|
||||
cleanup->base.fn = uploadWork_cleanup_fn;
|
||||
@@ -1312,7 +1340,7 @@ static ncclResult_t uploadWork(struct ncclComm* comm, struct ncclKernelPlan* pla
|
||||
cleanup->hostBuf = fifoBufHost;
|
||||
ncclIntruQueueEnqueue(&comm->eventCallbackQueue, (struct ncclCommEventCallback *)cleanup);
|
||||
|
||||
NCCLCHECKGOTO(ncclStrongStreamRelease(ncclCudaGraphNone(), &comm->sharedRes->deviceStream), result, fail);
|
||||
NCCLCHECKGOTO(ncclStrongStreamRelease(ncclCudaGraphNone(), &comm->sharedRes->deviceStream, /*concurrent=*/false), result, fail);
|
||||
NCCLCHECKGOTO(ncclCommPollEventCallbacks(comm), result, fail);
|
||||
|
||||
finish_scope:
|
||||
@@ -1386,15 +1414,38 @@ static void HIPRT_CB hostStreamPlanCallback(void *plan_) {
|
||||
if (result != ncclSuccess) {
|
||||
WARN("hostStreamPlanCallback() failed : %s", ncclGetErrorString(result));
|
||||
}
|
||||
if (!plan->persistent) ncclAtomicRefCountDecrement(&plan->comm->noncapturedRefs);
|
||||
if (!plan->persistent) ncclAtomicRefCountDecrement(&plan->comm->sharedRes->noncapturedRefs);
|
||||
return;
|
||||
}
|
||||
|
||||
static ncclResult_t reclaimPlan(struct ncclComm* comm, struct ncclCommCallback* me) {
|
||||
struct ncclKernelPlan* plan = (struct ncclKernelPlan*)me; // cast from first member `reclaim`
|
||||
if (plan->persistent) {
|
||||
comm->persistentRefs -= 1;
|
||||
NCCLCHECK(ncclCudaFree(plan->workBufPersistent));
|
||||
comm->sharedRes->persistentRefs -= 1;
|
||||
comm->localPersistentRefs -= 1;
|
||||
if (plan->workStorageType == ncclDevWorkStorageTypePersistent) {
|
||||
cudaStreamCaptureMode mode = cudaStreamCaptureModeRelaxed;
|
||||
CUDACHECK(cudaThreadExchangeStreamCaptureMode(&mode));
|
||||
CUDACHECK(cudaFree(plan->workBufPersistent));
|
||||
CUDACHECK(cudaThreadExchangeStreamCaptureMode(&mode));
|
||||
}
|
||||
}
|
||||
// Free coll tasks
|
||||
struct ncclTaskColl* ct = ncclIntruQueueHead(&plan->collTaskQueue);
|
||||
while (ct != nullptr) {
|
||||
struct ncclTaskColl* ct1 = ct->next;
|
||||
free(ct->sendNetHandles);
|
||||
free(ct->recvNetHandles);
|
||||
free(ct->srecvNetHandles);
|
||||
ncclMemoryPoolFree(&comm->memPool_ncclTaskColl, ct);
|
||||
ct = ct1;
|
||||
}
|
||||
// Free p2p tasks
|
||||
struct ncclTaskP2p* pt = ncclIntruQueueHead(&plan->p2pTaskQueue);
|
||||
while (pt != nullptr) {
|
||||
struct ncclTaskP2p* pt1 = pt->next;
|
||||
ncclMemoryPoolFree(&comm->memPool_ncclTaskP2p, pt);
|
||||
pt = pt1;
|
||||
}
|
||||
// Free proxy ops
|
||||
struct ncclProxyOp* q = ncclIntruQueueHead(&plan->proxyOpQueue);
|
||||
@@ -1427,6 +1478,32 @@ static void persistentDestructor(void* plans_) {
|
||||
}
|
||||
}
|
||||
|
||||
NCCL_PARAM(LaunchOrderImplicit, "LAUNCH_ORDER_IMPLICIT", 0);
|
||||
|
||||
namespace {
|
||||
enum ncclImplicitOrder {
|
||||
ncclImplicitOrderNone,
|
||||
ncclImplicitOrderSerial,
|
||||
ncclImplicitOrderLaunch
|
||||
};
|
||||
}
|
||||
|
||||
static ncclResult_t getImplicitOrder(enum ncclImplicitOrder *mode, bool capturing, int driver=-1) {
|
||||
if (ncclParamLaunchOrderImplicit()) {
|
||||
#if !defined(__HIP_PLATFORM_AMD__) || !defined(__HIPCC__)
|
||||
// Due to an unresolved bug in CUDA ncclImplicitOrderLaunch is not supported in graphs
|
||||
if (capturing) { *mode = ncclImplicitOrderSerial; return ncclSuccess; }
|
||||
if (driver < 0) { NCCLCHECK(ncclCudaDriverVersion(&driver)); }
|
||||
*mode = 12030 <= std::min<int>(CUDART_VERSION, driver) ? ncclImplicitOrderLaunch : ncclImplicitOrderSerial;
|
||||
#else
|
||||
*mode = ncclImplicitOrderNone;
|
||||
#endif
|
||||
return ncclSuccess;
|
||||
}
|
||||
*mode = ncclImplicitOrderNone;
|
||||
return ncclSuccess;
|
||||
}
|
||||
|
||||
ncclResult_t ncclLaunchPrepare(struct ncclComm* comm) {
|
||||
ncclResult_t result = ncclSuccess;
|
||||
struct ncclKernelPlanner* planner = &comm->planner;
|
||||
@@ -1474,63 +1551,65 @@ ncclResult_t ncclLaunchPrepare(struct ncclComm* comm) {
|
||||
|
||||
if (nPlans == 0) return ncclSuccess;
|
||||
|
||||
// Semantically we want these dependencies for the kernels launched:
|
||||
// 1. Launch host task on hostStream.
|
||||
// 2. Launch kernel, depends on all of {deviceStream, hostStream, userStream[i]...}
|
||||
// 3. {deviceStream, userStream[i]...} depend on kernel.
|
||||
// We achieve this by:
|
||||
// 1. userStream[0] waits on deviceStream
|
||||
// 2. deviceStream waits on each of userStream[1...]
|
||||
// 3. host task launch on hostStream
|
||||
// 4. userStream[0] waits on hostStream
|
||||
// 5. kernel launch on userStream[0]
|
||||
// 6. deviceStream waits on userStream[0]
|
||||
// 7. userStream[1...] each waits on deviceStream
|
||||
// The two-level fan-in fan-out is because ncclStrongStreamWaitStream() requires
|
||||
// at least one of the two streams to be strong-stream.
|
||||
cudaStream_t launchStream = planner->streams->stream;
|
||||
NCCLCHECKGOTO(ncclStrongStreamAcquire(planner->capturingGraph, &comm->sharedRes->deviceStream), result, failure);
|
||||
cudaStream_t deviceStream, launchOrder;
|
||||
NCCLCHECKGOTO(ncclStrongStreamAcquire(planner->capturingGraph, &comm->sharedRes->deviceStream, /*concurrent=*/false, &deviceStream), result, failure);
|
||||
|
||||
if (planner->numStreams != 1 || persistent) {
|
||||
// Create dependency for device stream on user streams. First from extra user
|
||||
// streams to deviceStream. Then deviceStream to first user stream.
|
||||
if (persistent || planner->numStreams != 1) {
|
||||
// userStream[0] waits on each userStream[i]...
|
||||
for (struct ncclCudaStreamList* l=planner->streams->next; l != nullptr; l = l->next) {
|
||||
NCCLCHECKGOTO(ncclStrongStreamWaitStream(planner->capturingGraph, &comm->sharedRes->deviceStream, l->stream), result, failure);
|
||||
CUDACHECKGOTO(cudaEventRecord(comm->sharedRes->scratchEvent, l->stream), result, failure);
|
||||
CUDACHECKGOTO(cudaStreamWaitEvent(launchStream, comm->sharedRes->scratchEvent, 0), result, failure);
|
||||
}
|
||||
NCCLCHECKGOTO(ncclStrongStreamWaitStream(planner->capturingGraph, launchStream, &comm->sharedRes->deviceStream), result, failure);
|
||||
// userStream[0] waits on deviceStream
|
||||
NCCLCHECKGOTO(ncclStreamWaitStream(launchStream, deviceStream, comm->sharedRes->scratchEvent), result, failure);
|
||||
} else if (planner->streams->stream != comm->lastStream && comm->lastStream != nullptr && !persistent) {
|
||||
// Stream changed from last call, create dependency against last NCCL kernel launch
|
||||
CUDACHECK(hipStreamWaitEvent(planner->streams->stream, comm->doneEvent, 0));
|
||||
CUDACHECKGOTO(hipStreamWaitEvent(planner->streams->stream, comm->doneEvent, 0), result, failure);
|
||||
}
|
||||
|
||||
if (persistent || comm->persistentRefs != 0 || ncclCudaLaunchBlocking || __atomic_load_n(&comm->noncapturedRefs, __ATOMIC_ACQUIRE)) {
|
||||
bool capturing = ncclCudaGraphValid(planner->capturingGraph);
|
||||
enum ncclImplicitOrder implicitOrder;
|
||||
NCCLCHECKGOTO(getImplicitOrder(&implicitOrder, capturing), result, failure);
|
||||
|
||||
if (implicitOrder != ncclImplicitOrderNone) {
|
||||
// userStream[0] waits on per-device (context) launchOrder. Concurrent strong stream access is
|
||||
// required if this is a graph capture, non-captured cannot be concurrent because that would violate
|
||||
// deterministic program order of launches.
|
||||
bool concurrent = capturing;
|
||||
NCCLCHECKGOTO(ncclStrongStreamAcquire(planner->capturingGraph, &comm->context->launchOrder, concurrent, &launchOrder), result, failure);
|
||||
NCCLCHECKGOTO(ncclStreamWaitStream(launchStream, launchOrder, comm->sharedRes->scratchEvent), result, failure);
|
||||
}
|
||||
|
||||
if (persistent || comm->sharedRes->persistentRefs != 0 || ncclCudaLaunchBlocking || __atomic_load_n(&comm->sharedRes->noncapturedRefs, __ATOMIC_ACQUIRE)) {
|
||||
// We have to launch host tasks to push proxy args. We are careful to only
|
||||
// do this if necessary since host tasks impose a high performance cost in CUDA.
|
||||
bool acquired = false;
|
||||
cudaStream_t hostStream;
|
||||
for (struct ncclKernelPlan* plan=planHead; plan != nullptr; plan = plan->next) {
|
||||
if (plan->hasProxyOps) {
|
||||
if (!acquired) {
|
||||
acquired = true;
|
||||
NCCLCHECKGOTO(ncclStrongStreamAcquire(planner->capturingGraph, &comm->sharedRes->hostStream), result, failure);
|
||||
NCCLCHECKGOTO(ncclStrongStreamAcquire(planner->capturingGraph, &comm->sharedRes->hostStream, /*concurrent=*/false, &hostStream), result, failure);
|
||||
}
|
||||
if (!persistent) ncclAtomicRefCountIncrement(&comm->noncapturedRefs);
|
||||
if (!persistent) ncclAtomicRefCountIncrement(&comm->sharedRes->noncapturedRefs);
|
||||
plan->isHostCbEnq = true;
|
||||
NCCLCHECKGOTO(ncclStrongStreamLaunchHost(planner->capturingGraph, &comm->sharedRes->hostStream, hostStreamPlanCallback, plan), result, failure);
|
||||
CUDACHECKGOTO(cudaLaunchHostFunc(hostStream, hostStreamPlanCallback, plan), result, failure);
|
||||
}
|
||||
}
|
||||
if (acquired) {
|
||||
// Make to-be-launched kernels dependent on just-launched host stream tasks.
|
||||
NCCLCHECKGOTO(ncclStrongStreamWaitStream(planner->capturingGraph, launchStream, &comm->sharedRes->hostStream), result, failure);
|
||||
NCCLCHECKGOTO(ncclStrongStreamRelease(planner->capturingGraph, &comm->sharedRes->hostStream), result, failure);
|
||||
NCCLCHECKGOTO(ncclStreamWaitStream(launchStream, hostStream, comm->sharedRes->scratchEvent), result, failure);
|
||||
NCCLCHECKGOTO(ncclStrongStreamRelease(planner->capturingGraph, &comm->sharedRes->hostStream, /*concurrent=*/false), result, failure);
|
||||
}
|
||||
}
|
||||
|
||||
if (persistent) {
|
||||
comm->persistentRefs += nPlans;
|
||||
comm->sharedRes->persistentRefs += nPlans;
|
||||
comm->localPersistentRefs += nPlans;
|
||||
NCCLCHECKGOTO(ncclCudaGraphAddDestructor(planner->capturingGraph, persistentDestructor, (void*)planHead), result, failure);
|
||||
}
|
||||
}
|
||||
|
||||
failure:
|
||||
return result;
|
||||
}
|
||||
@@ -1549,6 +1628,7 @@ NCCL_PARAM(MemSyncDomain, "MEM_SYNC_DOMAIN", cudaLaunchMemSyncDomainRemote);
|
||||
#endif
|
||||
|
||||
ncclResult_t ncclLaunchKernel(struct ncclComm* comm, struct ncclKernelPlan* plan) {
|
||||
ncclResult_t ret = ncclSuccess;
|
||||
struct ncclKernelPlanner* planner = &comm->planner;
|
||||
int nChannels = 0;
|
||||
for (int i = 0; i < MAXCHANNELS/64; i++)
|
||||
@@ -1561,23 +1641,28 @@ ncclResult_t ncclLaunchKernel(struct ncclComm* comm, struct ncclKernelPlan* plan
|
||||
void* extra[] = {plan->kernelArgs, &plan->kernelArgsSize};
|
||||
|
||||
if (planner->numStreams == 1 && !plan->persistent) {
|
||||
CUDACHECK(hipExtLaunchKernel(plan->kernelFn, grid, block, extra, 0, launchStream, NULL, comm->doneEvent, 0));
|
||||
comm->lastStream = planner->streams->stream;
|
||||
CUDACHECKGOTO(hipExtLaunchKernel(plan->kernelFn, grid, block, extra, 0, launchStream, NULL, comm->doneEvent, 0), ret, do_return);
|
||||
return ncclSuccess;
|
||||
}
|
||||
|
||||
// CUfunction fn;
|
||||
// CUDACHECK(cudaGetFuncBySymbol(&fn, sym));
|
||||
|
||||
#if CUDART_VERSION >= 11080
|
||||
#if !defined(__HIP_PLATFORM_AMD__) || !defined(__HIPCC__)
|
||||
int driverVersion;
|
||||
NCCLCHECK(ncclCudaDriverVersion(&driverVersion));
|
||||
if (driverVersion >= 11080) {
|
||||
NCCLCHECKGOTO(ncclCudaDriverVersion(&driverVersion), ret, do_return);
|
||||
|
||||
CUfunction fn;
|
||||
CUDACHECKGOTO(cudaGetFuncBySymbol(&fn, sym), ret, do_return);
|
||||
|
||||
if (CUDART_VERSION >= 11080 && driverVersion >= 11080) {
|
||||
#if CUDART_VERSION >= 11080
|
||||
int compCap = comm->compCap;
|
||||
unsigned int clusterSize = (compCap >= 90) ? comm->config.cgaClusterSize : 0;
|
||||
|
||||
CUlaunchConfig launchConfig = {0};
|
||||
CUlaunchAttribute launchAttrs[3];
|
||||
CUlaunchAttribute launchAttrs[4] = {};
|
||||
int attrs = 0;
|
||||
/* Cooperative Group Array (CGA)
|
||||
* On sm90 and later we have an extra level of hierarchy where we
|
||||
@@ -1604,6 +1689,17 @@ ncclResult_t ncclLaunchKernel(struct ncclComm* comm, struct ncclKernelPlan* plan
|
||||
launchAttrs[attrs++].value.memSyncDomain = (CUlaunchMemSyncDomain) ncclParamMemSyncDomain();
|
||||
}
|
||||
#endif
|
||||
#if CUDART_VERSION >= 12030
|
||||
bool capturing = ncclCudaGraphValid(planner->capturingGraph);
|
||||
enum ncclImplicitOrder implicitOrder;
|
||||
NCCLCHECKGOTO(getImplicitOrder(&implicitOrder, capturing, driverVersion), ret, do_return);
|
||||
if (implicitOrder == ncclImplicitOrderLaunch) {
|
||||
launchAttrs[attrs].id = CU_LAUNCH_ATTRIBUTE_LAUNCH_COMPLETION_EVENT;
|
||||
launchAttrs[attrs].value.launchCompletionEvent.event = comm->sharedRes->launchEvent;
|
||||
launchAttrs[attrs].value.launchCompletionEvent.flags = 0;
|
||||
attrs++;
|
||||
}
|
||||
#endif
|
||||
launchConfig.gridDimX = grid.x;
|
||||
launchConfig.gridDimY = grid.y;
|
||||
launchConfig.gridDimZ = grid.z;
|
||||
@@ -1615,15 +1711,19 @@ ncclResult_t ncclLaunchKernel(struct ncclComm* comm, struct ncclKernelPlan* plan
|
||||
launchConfig.numAttrs = attrs;
|
||||
launchConfig.hStream = launchStream;
|
||||
|
||||
//CUDACHECK(cudaLaunchKernelExC(&launchConfig, fnAddr, args));
|
||||
CUCHECK(cuLaunchKernelEx(&launchConfig, fn, nullptr, extra));
|
||||
return ncclSuccess;
|
||||
}
|
||||
CUCHECKGOTO(cuLaunchKernelEx(&launchConfig, fn, nullptr, extra), ret, do_return);
|
||||
#endif
|
||||
} else {
|
||||
// Standard kernel launch
|
||||
CUCHECKGOTO(cuLaunchKernel(fn, grid.x, grid.y, grid.z, block.x, block.y, block.z, smem, launchStream, nullptr, extra), ret, do_return);
|
||||
}
|
||||
#endif
|
||||
// Standard kernel launch
|
||||
//cuLaunchKernel(sym, grid.x, grid.y, grid.z, block.x, block.y, block.z, smem, launchStream, nullptr, extra);
|
||||
CUDACHECK(cudaLaunchKernel(sym, grid, block, extra, smem, launchStream));
|
||||
return ncclSuccess;
|
||||
CUDACHECKGOTO(cudaLaunchKernel(sym, grid, block, extra, smem, launchStream), ret, do_return);
|
||||
|
||||
do_return:
|
||||
return ret;
|
||||
}
|
||||
|
||||
ncclResult_t ncclLaunchKernelAfter_NoCuda(struct ncclComm* comm, struct ncclKernelPlan* plan) {
|
||||
@@ -1643,36 +1743,51 @@ ncclResult_t ncclLaunchKernelAfter_NoCuda(struct ncclComm* comm, struct ncclKern
|
||||
}
|
||||
|
||||
ncclResult_t ncclLaunchFinish(struct ncclComm* comm) {
|
||||
ncclResult_t result = ncclSuccess;
|
||||
struct ncclKernelPlanner* planner = &comm->planner;
|
||||
bool persistent = ncclCudaGraphValid(planner->capturingGraph);
|
||||
|
||||
if (!ncclIntruQueueEmpty(&planner->planQueue)) {
|
||||
// Reset queue to empty without destroying plans since those will be sent
|
||||
// back to us for reclaiming via callbackQueue.
|
||||
ncclIntruQueueConstruct(&planner->planQueue);
|
||||
|
||||
bool capturing = ncclCudaGraphValid(planner->capturingGraph);
|
||||
cudaStream_t launchStream = planner->streams->stream; // First user stream gets launch
|
||||
// Create dependency for deviceStream on launchStream. We know that deviceStream
|
||||
// hasn't been modified since launchStream waited on it (in ncclLaunchPrepare),
|
||||
// so we can say that launchStream subsumes it.
|
||||
if (persistent || planner->numStreams != 1) NCCLCHECKGOTO(ncclStrongStreamWaitStream(planner->capturingGraph, &comm->sharedRes->deviceStream, launchStream, /*b_subsumes_a=*/true), result, resume1);
|
||||
resume1:
|
||||
// Create dependency for other user streams (skip launch stream) on deviceStream.
|
||||
// Again, the user streams haven't been touched since deviceStream waited on them
|
||||
// so we can say they are subsumed by deviceStream.
|
||||
struct ncclCudaStreamList* sl = planner->streams->next;
|
||||
planner->streams = nullptr; // Reset comm->planner.streams to empty.
|
||||
while (sl != nullptr && (planner->numStreams != 1 || persistent)) {
|
||||
NCCLCHECKGOTO(ncclStrongStreamWaitStream(planner->capturingGraph, sl->stream, &comm->sharedRes->deviceStream, /*b_subsumes_a=*/true), result, resume2);
|
||||
resume2:
|
||||
sl = sl->next;
|
||||
cudaStream_t deviceStream, launchOrder;
|
||||
|
||||
if (capturing || planner->numStreams != 1) {
|
||||
// CUDACHECK(cudaEventRecord(comm->sharedRes->scratchEvent, launchStream));
|
||||
// deviceStream waits on userStream[0]
|
||||
NCCLCHECK(ncclStrongStreamAcquiredWorkStream(planner->capturingGraph, &comm->sharedRes->deviceStream, /*concurrent=*/false, &deviceStream));
|
||||
|
||||
// We know that deviceStream is strictly behind the launchStream because launchStream
|
||||
// synced with it before kernel launch. This allows us to to see deviceStream waiting
|
||||
// on launchStream as a fast-forward. When building CUDA graphs fast forwards should
|
||||
// be handled specially so as not to create graphs with a blowup in the number of edges.
|
||||
// So we could do this:
|
||||
// CUDACHECK(cudaStreamWaitEvent(deviceStream, comm->sharedRes->scratchEvent, 0));
|
||||
// But instead we do:
|
||||
NCCLCHECK(ncclStreamAdvanceToEvent(planner->capturingGraph, deviceStream, comm->sharedRes->scratchEvent));
|
||||
|
||||
// Each userStream[i] waits on userStream[0]
|
||||
for (struct ncclCudaStreamList* l=planner->streams->next; l != nullptr; l = l->next) {
|
||||
CUDACHECK(cudaStreamWaitEvent(l->stream, comm->sharedRes->scratchEvent, 0));
|
||||
}
|
||||
}
|
||||
planner->numStreams = 0;
|
||||
// Release device stream as acquired in ncclLaunchPrepare()
|
||||
NCCLCHECKGOTO(ncclStrongStreamRelease(planner->capturingGraph, &comm->sharedRes->deviceStream), result, resume3);
|
||||
resume3:;
|
||||
enum ncclImplicitOrder implicitOrder;
|
||||
NCCLCHECK(getImplicitOrder(&implicitOrder, capturing));
|
||||
if (implicitOrder != ncclImplicitOrderNone) {
|
||||
// As in ncclLaunchPrepare, strong stream can be non-concurrent when non-captured.
|
||||
bool concurrent = capturing;
|
||||
// Incorporate launch event into per-device (context) launch order.
|
||||
NCCLCHECK(ncclStrongStreamAcquiredWorkStream(planner->capturingGraph, &comm->context->launchOrder, concurrent, &launchOrder));
|
||||
// If we don't have launch events (requires CUDA 12.3) then just use completion event (serialize execution).
|
||||
CUDACHECK(cudaStreamWaitEvent(launchOrder, implicitOrder == ncclImplicitOrderLaunch ? comm->sharedRes->launchEvent : comm->sharedRes->scratchEvent));
|
||||
// Release launchOrder as acquired in ncclLaunchPrepare()
|
||||
NCCLCHECK(ncclStrongStreamRelease(planner->capturingGraph, &comm->context->launchOrder, concurrent));
|
||||
}
|
||||
// Release deviceStream as acquired in ncclLaunchPrepare()
|
||||
NCCLCHECK(ncclStrongStreamRelease(planner->capturingGraph, &comm->sharedRes->deviceStream, /*concurrent=*/false));
|
||||
}
|
||||
return result;
|
||||
return ncclSuccess;
|
||||
}
|
||||
|
||||
/*****************************************************************************/
|
||||
@@ -1780,11 +1895,11 @@ static ncclResult_t topoGetAlgoInfo(
|
||||
if (info->algorithm == NCCL_ALGO_UNDEF || info->protocol == NCCL_PROTO_UNDEF) {
|
||||
char ncclAlgoEnvStr[1024] = "";
|
||||
char ncclProtoEnvStr[1024] = "";
|
||||
char* algoEnv = getenv("NCCL_ALGO");
|
||||
const char* algoEnv = ncclGetEnv("NCCL_ALGO");
|
||||
if (algoEnv) {
|
||||
snprintf(ncclAlgoEnvStr, 1023, " NCCL_ALGO was set to %s.", algoEnv);
|
||||
}
|
||||
char* protoEnv = getenv("NCCL_PROTO");
|
||||
const char* protoEnv = ncclGetEnv("NCCL_PROTO");
|
||||
if (protoEnv) {
|
||||
snprintf(ncclProtoEnvStr, 1023, " NCCL_PROTO was set to %s.", protoEnv);
|
||||
}
|
||||
@@ -2265,12 +2380,13 @@ static ncclResult_t taskAppend(struct ncclComm* comm, struct ncclInfo* info) {
|
||||
|
||||
// Must be in thread local group before tasks can be alloc'd in `comm->memScoped`.
|
||||
ncclGroupCommJoin(info->comm);
|
||||
struct ncclTaskP2p* p2p = ncclMemoryStackAlloc<struct ncclTaskP2p>(&comm->memScoped);
|
||||
struct ncclTaskP2p* p2p = ncclMemoryPoolAlloc<struct ncclTaskP2p>(&comm->memPool_ncclTaskP2p, &comm->memPermanent);
|
||||
p2p->buff = (void*)info->recvbuff;
|
||||
p2p->count = info->count;
|
||||
p2p->datatype = info->datatype;
|
||||
p2p->root = info->root;
|
||||
p2p->bytes = nBytes;
|
||||
p2p->eActivationMask = __atomic_load_n(&ncclProfilerEventMask, __ATOMIC_RELAXED);
|
||||
p2p->opCount = comm->opCount;
|
||||
ncclIntruQueueEnqueue(
|
||||
isSendNotRecv ? &planner->peers[peer].sendQueue : &planner->peers[peer].recvQueue,
|
||||
@@ -2280,6 +2396,7 @@ static ncclResult_t taskAppend(struct ncclComm* comm, struct ncclInfo* info) {
|
||||
// Mark channels that need pre-connect
|
||||
if (comm->rank != peer) {
|
||||
if (!(isSendNotRecv ? planner->peers[peer].sendSeen : planner->peers[peer].recvSeen)) {
|
||||
// planner->peers[peer].send/recvSeen is private to each comm, so we need to set it anyway.
|
||||
(isSendNotRecv ? planner->peers[peer].sendSeen : planner->peers[peer].recvSeen) = true;
|
||||
int round = 0;
|
||||
while (peer != (isSendNotRecv ? comm->p2pSchedule[round].sendRank
|
||||
@@ -2290,23 +2407,30 @@ static ncclResult_t taskAppend(struct ncclComm* comm, struct ncclInfo* info) {
|
||||
for (int c=0; c < comm->p2pnChannelsPerPeer; c++) {
|
||||
int channelId = ncclP2pChannelForPart(comm->p2pnChannels, base, c, comm->p2pnChannelsPerPeer, comm->nNodes);
|
||||
if (isSendNotRecv) {
|
||||
if (comm->channels[channelId].peers[peer]->send[1].connected == 0) { // P2P uses only 1 connector
|
||||
if (comm->channels[channelId].peers[peer]->send[1].hasSeen == 0) { // P2P uses only 1 connector
|
||||
// the send/recv connector is shared among split shared comms. We need to set hasSeen to
|
||||
// 1 in order to avoid duplicate connection setup if user group sendrecv ops with split
|
||||
// shared comms together.
|
||||
comm->channels[channelId].peers[peer]->send[1].hasSeen = 1;
|
||||
//comm->connectSend[peer] |= (1UL<<channelId);
|
||||
comm->connectSend[peer].masks[channelId/64] |= (1UL<<(channelId%64));
|
||||
ncclGroupCommPreconnect(comm);
|
||||
}
|
||||
if (comm->p2pNet && comm->channels[channelId].peers[peer]->send[NCCL_CONN_IDX_P2P_NET].connected == 0) {
|
||||
if (comm->p2pNet && comm->channels[channelId].peers[peer]->send[NCCL_CONN_IDX_P2P_NET].hasSeen == 0) {
|
||||
comm->channels[channelId].peers[peer]->send[1].hasSeen = 1;
|
||||
//comm->connectSend[peer+comm->nRanks*NCCL_CONN_IDX_P2P_NET] |= (1UL<<channelId);
|
||||
comm->connectSend[peer+comm->nRanks*NCCL_CONN_IDX_P2P_NET].masks[channelId/64] |= (1UL<<(channelId%64));
|
||||
ncclGroupCommPreconnect(comm);
|
||||
}
|
||||
} else {
|
||||
if (comm->channels[channelId].peers[peer]->recv[1].connected == 0) { // P2P uses only 1 connector
|
||||
if (comm->channels[channelId].peers[peer]->recv[1].hasSeen == 0) { // P2P uses only 1 connector
|
||||
comm->channels[channelId].peers[peer]->recv[1].hasSeen = 1;
|
||||
//comm->connectRecv[peer] |= (1UL<<channelId);
|
||||
comm->connectRecv[peer].masks[channelId/64] |= (1UL<<(channelId%64));
|
||||
ncclGroupCommPreconnect(comm);
|
||||
}
|
||||
if (comm->p2pNet && comm->channels[channelId].peers[peer]->recv[NCCL_CONN_IDX_P2P_NET].connected == 0) {
|
||||
if (comm->p2pNet && comm->channels[channelId].peers[peer]->recv[NCCL_CONN_IDX_P2P_NET].hasSeen == 0) {
|
||||
comm->channels[channelId].peers[peer]->recv[1].hasSeen = 1;
|
||||
//comm->connectRecv[peer+comm->nRanks*NCCL_CONN_IDX_P2P_NET] |= (1UL<<channelId);
|
||||
comm->connectRecv[peer+comm->nRanks*NCCL_CONN_IDX_P2P_NET].masks[channelId/64] |= (1UL<<(channelId%64));
|
||||
ncclGroupCommPreconnect(comm);
|
||||
@@ -2337,7 +2461,7 @@ static ncclResult_t taskAppend(struct ncclComm* comm, struct ncclInfo* info) {
|
||||
} else {
|
||||
// Must be in thread local group before tasks can be alloc'd in `comm->memScoped`.
|
||||
ncclGroupCommJoin(info->comm);
|
||||
struct ncclTaskColl* t = ncclMemoryStackAlloc<struct ncclTaskColl>(&comm->memScoped);
|
||||
struct ncclTaskColl* t = ncclMemoryPoolAlloc<struct ncclTaskColl>(&comm->memPool_ncclTaskColl, &comm->memPermanent);
|
||||
t->func = info->coll;
|
||||
t->sendbuff = info->sendbuff;
|
||||
t->recvbuff = info->recvbuff;
|
||||
@@ -2355,6 +2479,7 @@ static ncclResult_t taskAppend(struct ncclComm* comm, struct ncclInfo* info) {
|
||||
t->opDev = opDev; // C++ struct assignment
|
||||
t->chunkSteps = info->chunkSteps;
|
||||
t->sliceSteps = info->sliceSteps;
|
||||
t->eActivationMask = __atomic_load_n(&ncclProfilerEventMask, __ATOMIC_RELAXED);
|
||||
t->opCount = comm->opCount;
|
||||
|
||||
planner->nTasksColl += 1;
|
||||
|
||||
Reference in New Issue
Block a user