代码解读基于版本2.11.4;后续会基于最新版本重构,敬请期待!
NCCL里数据通信是通过CUDA kernel执行的, 其中每一个channel会绑定一个block(也就是一个SM). kernel的代码在src/collectives/device
目录的各个.h中,
包含了all_reduce,all_gather等各种通信原语, 我们就以all_reduce入手, 着重讲讲其原理, 其他原语比较类似, 有区别的地方会讲到. 另外all_reduce原语还分别有ring及tree算法, 每种算法实现还有simple/ll(low latency)/ll_128三种协议
我们接下来以all_reduce原语的ring算法中的simple协议的实现入手, 逐行解析, 后面会对其中不一样的地方进行补充
我们首先需要了解ring all_reduce的实现原理( ‣), 假设当前有4张卡, 有4个channel进行数据传输, 从上帝视角看, 其传输过程如下图:
ring all-reduce示意图
这时候我们从时序的角度, 看下每张卡做的事情:
也就是说在任意时刻, 任意一张卡都是在做send/recv, 而且其要发送的rank及接收的rank是确定且固定的.
接下来我们看下代码
template<typename T, typename RedOp, typename Proto>
__device__ __forceinline__ void runRing(ncclWorkElem *args) {
const int tid = threadIdx.x;
const int nthreads = args->nThreads;
const int bid = args->coll.bid;
const int nChannels = args->coll.nChannels;
ncclRing *ring = &ncclShmem.channel.ring;
int ringIx = ring->index;
const ssize_t chunkSize = int(Proto::calcBytePerStep()/sizeof(T) * (Proto::Id == NCCL_PROTO_SIMPLE ? ALLREDUCE_CHUNKSTEPS : 1));
const int nranks = ncclShmem.comm.nRanks;
const ssize_t loopSize = nChannels*nranks*chunkSize;
const ssize_t size = args->coll.count;
这里分别解释下含义: