NCCL和MPI
API
NCCL的API和用法与MPI类似,但存在许多细微的差异。以下列表总结了这些差异:
在一个进程中使用多个设备
与MPI端点的概念类似,NCCL不需要将等级映射为1:1的MPI等级。
一个NCCL通信器可以有多个等级与单个进程关联(如果与MPI一起使用,则是MPI等级)。
ReduceScatter操作
ncclReduceScatter操作类似于MPI_Reduce_scatter_block操作,而不是MPI_Reduce_scatter操作。
MPI_Reduce_scatter函数在本质上是一个“向量”函数,而MPI_Reduce_scatter_block函数(后面定义以填充缺失的语义)与镜像函数MPI_Allgather类似地提供了规则的计数。
这是MPI的奇特之处,出于合法的向后兼容性原因,尚未修复,并且NCCL不遵循这一规定。
发送和接收计数
在许多集合操作中,MPI允许不同的发送和接收计数和类型,只要sendcount * sizeof(sendtype) == recvcount * sizeof(recvtype)。NCCL不允许这样做,只定义了一个计数和一个数据类型。
对于AllGather和ReduceScatter操作,计数等于每个等级的大小,即最小大小;另一个计数等于nranks * count。函数的原型清楚地显示了提供的计数。ncclAllgather作为参数具有sendcount,而ncclReduceScatter作为参数具有recvcount。
注意:当使用ReduceScatter和AllGather的组合进行AllReduce操作时,将总计数除以等级数,并进行正确的计数舍入,如果它不是等级数的完美倍数,则定义sendcount和recvcount。
其他集合和点对点操作
NCCL不为sendrecv、gather、gatherv、scatter、scatterv、alltoall、alltoallv、alltoallw和neighbor集合定义特定的动词。所有这些操作都可以简单地使用ncclSend、ncclRecv和ncclGroupStart/ncclGroupEnd的组合来表达,类似于如何使用MPI_Isend、MPI_Irecv和MPI_Waitall表达它们。
原地操作
有关更多信息,请参见in-place-operations
。
在MPI程序中使用NCCL
NCCL可以很容易地与MPI一起使用。NCCL集合与MPI集合类似,因此,将MPI通信器创建为NCCL通信器很简单。因此,可以使用MPI进行CPU到CPU通信,使用NCCL进行GPU到GPU通信。
但是,在MPI中的一些实现细节在使用NCCL时可能会导致问题。
MPI Progress
MPI定义了一个progress的概念,这意味着MPI操作需要程序调用MPI函数(可能多次)进行progress并最终完成。
在某些实现中,一个等级上的progress可能需要在另一个等级上调用MPI。虽然这通常对性能不利,但可以认为这是一个有效的MPI实现。
因此,在NCCL集合操作中阻塞,例如调用cudaStreamSynchronize,在某些情况下可能会导致死锁,因为不调用MPI将不会使其他等级progress,因此无法达到NCCL调用,从而解除NCCL操作的阻塞。
在这种情况下,cudaStreamSynchronize的调用应该被以下循环替换:
cudaError_t err = cudaErrorNotReady;
int flag;
while (err == cudaErrorNotReady) {
err = cudaStreamQuery(args->streams[i]);
MPI_Iprobe(MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &flag, MPI_STATUS_IGNORE);
}
使用CUDA-aware MPI进行Inter-GPU通信
在使用CUDA-aware MPI同时执行GPU间通信时,使用NCCL可能会导致死锁。
NCCL创建了 跨设备的依赖关系,这意味着在它被启动后,一个NCCL内核会等待(并且可能会阻塞CUDA设备),直到通信器中的所有等级都启动了它们的NCCL内核。根据MPI实现的不同,CUDA-aware MPI也可能在设备之间创建这样的依赖关系。
因此,同时使用MPI和NCCL在相同的一组CUDA设备之间进行传输并不保证是安全的。