跳到主要内容

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设备之间进行传输并不保证是安全的。