NCCL 集合通信--Collective Operations

本文详细介绍了NVIDIA Collective Operations中的AllReduce、Broadcast、Reduce和AllGather四种通信操作,包括它们的原理、示例和RingAllReduce、RingReduceScatter、RingAllGather等实际实现策略。这些技术在深度学习和分布式计算中至关重要。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

集合通信 Collective Operations

AllReduce

在这里插入图片描述

ncclResult_t ncclBroadcast(const void* sendbuff, void* recvbuff, size_t count, ncclDataType_t datatype, int root, ncclComm_t comm, cudaStream_t stream)
  • N 个设备参加的AllReduce,没有root,初始状态下,所有的N个设备上sendbuffer 长度为 count(函数中定义 size_t count),N个sendbuffer中的内容记为 in_0,in_1,…,in_N-1, 运行结束后,所有sendbuffer中内容经过op操作,使得N个设备上的recv buffer中有相同的结果副本,长度均为count,即out。
  • 以op操作为ncclSum为例, out[i] 来自于所有sendbuffer的 in_k[i] 的和,即 out[i] = sum(in_k[i])

Broadcast

在这里插入图片描述

ncclResult_t ncclBroadcast(const void* sendbuff, void* recvbuff, size_t count, ncclDataType_t datatype, int root, ncclComm_t comm, cudaStream_t stream)

N个设备参与的broadcast,root 是 函数中指定的 int root(以上图为例则是2),初始状态下,rank 为 root(上图中2)的设备上的send buffer 长度为count。运行结束后,所有参与broadcast 的N个设备上的recv buffer 长度为 count, 且所有N个rank 的 recv buffer 部分的数据均来自于 root设备的send buffer 的拷贝。

Reduce

在这里插入图片描述

ncclResult_t ncclReduce(const void* sendbuff, void* recvbuff, size_t count, ncclDataType_t datatype, ncclRedOp_t op, int root, ncclComm_t comm, cudaStream_t stream)
  • N个设备参与的reduce ,root 是 函数中指定的 int root(上图中为2),初始状态下,N个设备上的send buffer 长度为 count,记为数组 in_0,in_1,…in_N-1。 运行结束后,rank 为 root (上图中为2)的设备的recv buffer长度为count,记为数组out 。其中 recv buffer的数据来自于N个的send buffer经op操作后的结果。
  • 以op操作为 ncclSum为例,out[i] 为各个send buffer 中 in_X[i] 的和,即out[i] = sum(in_X[i])

AllGather

在这里插入图片描述

ncclResult_t ncclAllGather(const void* sendbuff, void* recvbuff, size_t sendcount, ncclDataType_t datatype, ncclComm_t comm, cudaStream_t stream)

N个设备参与AllGather,没有root,初始状态下,N个设备上的send buffer 长度为 sendcount,记为数组 in_0,int_1,…,in_N-1,运行结束后,N个设备上的recv buffer 内容均相同,长度均为 N ∗ s e n d c o u n t N*sendcount Nsendcount,记为 数组out 。其中每个设备上的recv buffer中的 o u t [ Y ∗ s e n d c o u t + i ] out[Y*sendcout + i] out[Ysendcout+i]来自于rank为 Y 的设备的send buffer中的 in_Y[i] , 即 out[Y*count + i] = in_Y[i]

ReduceScatter

在这里插入图片描述

ncclResult_t ncclReduceScatter(const void* sendbuff, void* recvbuff, size_t recvcount, ncclDataType_t datatype, ncclRedOp_t op, ncclComm_t comm, cudaStream_t stream)
  • N 个设备参与的ReduceScatter,没有root设备。初始状态下,N个设备的sendbuffer 长度均为 N*recvcount。运行结束后,N个设备的recv buffer长度为 recvcount,记为数组out_0,out_1,…,out_N-1。其中,N个 send buffer中数据经过op 操作得到 reduced result,将其散布在 N 个设备中,每个设备仅包含 reduced result中一部分。
  • 以op操作为 ncclSum为例, rank为Y的设备中数据 out_Y[i] 为 reduced result 中 Yrecvcount+i 位置的数据,即 out_Y[i] = sum(in_X[Ycount + i])

集合通信的实现

Ring AllReduce

Ring AllReduce

各个设备首尾相连,形成单向的环。每个环上处理一部分数据 block,NCCL 在 luanch kernel 时,会确定 block的数量,一个block对应一个环。

一个循环中AllReduce的实现:

  • 一共有 k 各设备,将每个设备上的数据划分成等大的 k 个 chunk
    在这里插入图片描述

  • step 0 当前设备的 rank 为 ringIx 则将 (ringIx + k -1 ) mod N 号 chunk 传给下一个设备

在这里插入图片描述

 // step 0: push data to next GPU
      chunk = modRanks(ringIx + nranks-1);
      offset = calcOffset(chunk);
      nelem = min(realChunkSize, size-offset);
      prims.send(offset, nelem);
  • 进行 K - 2 次循环,j 从2 遍历道 N-1; 每次循环将 接收该设备的 (ringIx + k - j ) mod N 作为 chunk_id ,进行reduce之后,传给下一个设备

在这里插入图片描述

 // k-2 steps: reduce and copy to next GPU
      for (int j=2; j<nranks; ++j) {
        chunk = modRanks(ringIx + nranks-j);
        offset = calcOffset(chunk);
        nelem = min(realChunkSize, size-offset);
        prims.recvReduceSend(offset, nelem);    
      }
  • step k-1 每个设备选择自己的 rank 作为 chunk_id,调用 directRecvReduceCopySend 接收 chunk,和 sendbuff 中对应的数据 reduce,此时的结果已经是该 chunk 所有 rank sendbuff 中的数据 reduce 之后的结果,将这个结果写入到 recvbuff ,并发送给下一个设备,因为下一步操作不需要再进行 reduce,所以可以使用 directSend。
    在这里插入图片描述
 // step k-1: reduce this buffer and data, which will produce the final
      // result that we store in this data and push to the next GPU
      chunk = ringIx + 0;
      offset = calcOffset(chunk);
      nelem = min(realChunkSize, size-offset);
      prims.directRecvReduceCopySend(offset, offset, offset, nelem, /*postOp=*/true);
  • k-2 step 接收对应位置数据并发送给下一个设备

在这里插入图片描述
在这里插入图片描述

// k-2 steps: copy to next GPU
      for (int j=1; j<nranks-1; ++j) {
        chunk = modRanks(ringIx + nranks-j);
        offset = calcOffset(chunk);
        nelem = min(realChunkSize, size-offset);
        prims.directRecvCopySend(offset, offset, nelem);
      }
  • 最后一步只需要接收数据,不需要发送

在这里插入图片描述

// Make final copy from buffer to dest.
      chunk = modRanks(ringIx + 1);
      offset = calcOffset(chunk);
      nelem = min(realChunkSize, size-offset);
      prims.directRecv(offset, nelem);

Ring ReduceScatter

和Ring AllReduce 前半部分很相似

在这里插入图片描述

 /// begin ReduceScatter steps ///
      ssize_t offset;
      int nelem = min(realChunkSize, size-chunkOffset);
      int rankDest;

      // step 0: push data to next GPU
      rankDest = ringRanks[nranks-1];
      offset = chunkOffset + rankDest * size;
      prims.send(offset, nelem);

在这里插入图片描述

 // k-2 steps: reduce and copy to next GPU
      for (int j=2; j<nranks; ++j) {
        rankDest = ringRanks[nranks-j];
        offset = chunkOffset + rankDest * size;
        prims.recvReduceSend(offset, nelem);
      }

在这里插入图片描述

  // step k-1: reduce this buffer and data, which will produce the final result
      rankDest = ringRanks[0];
      offset = chunkOffset + rankDest * size;
      prims.recvReduceCopy(offset, chunkOffset, nelem, /*postOp=*/true);

Ring AllGather

allGathre 和 allReduce 后半段过程很相似

在这里插入图片描述

/// begin AllGather steps ///
      ssize_t offset;
      int nelem = min(realChunkSize, size-chunkOffset);
      int rankDest;

      // step 0: push data to next GPU
      rankDest = ringRanks[0];
      offset = chunkOffset + rankDest * size;

在这里插入图片描述

 // k-2 steps: copy to next GPU
      for (int j=1; j<nranks-1; ++j) {
        rankDest = ringRanks[nranks-j];
        offset = chunkOffset + rankDest * size;

        prims.directRecvCopySend(offset, offset, nelem);
      }

在这里插入图片描述

// Make final copy from buffer to dest.
      rankDest = ringRanks[1];
      offset = chunkOffset + rankDest * size;

      // Final wait/copy.
      prims.directRecv(offset, nelem);

Ring Reduce

在这里插入图片描述

if (prevRank == root) {
      for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
        int realChunkSize = calcChunkSize(gridOffset);
        ssize_t offset = gridOffset + bid*realChunkSize;
        int nelem = min(realChunkSize, size-offset);
        prims.send(offset, nelem);
      }
    }

在这里插入图片描述

在这里插入图片描述

 for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
        int realChunkSize = calcChunkSize(gridOffset);
        ssize_t offset = gridOffset + bid*realChunkSize;
        int nelem = min(realChunkSize, size-offset);
        prims.recvReduceSend(offset, nelem);
      }

在这里插入图片描述

 else if (rank == root) {
      for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
        int realChunkSize = calcChunkSize(gridOffset);
        ssize_t offset = gridOffset + bid*realChunkSize;
        int nelem = min(realChunkSize, size-offset);
        prims.recvReduceCopy(offset, offset, nelem, /*postOp=*/true);
      }
    }

总的代码:

 if (prevRank == root) {
      for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
        int realChunkSize = calcChunkSize(gridOffset);
        ssize_t offset = gridOffset + bid*realChunkSize;
        int nelem = min(realChunkSize, size-offset);
        prims.send(offset, nelem);
      }
    }
    else if (rank == root) {
      for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
        int realChunkSize = calcChunkSize(gridOffset);
        ssize_t offset = gridOffset + bid*realChunkSize;
        int nelem = min(realChunkSize, size-offset);
        prims.recvReduceCopy(offset, offset, nelem, /*postOp=*/true);
      }
    }
    else {
      for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
        int realChunkSize = calcChunkSize(gridOffset);
        ssize_t offset = gridOffset + bid*realChunkSize;
        int nelem = min(realChunkSize, size-offset);
        prims.recvReduceSend(offset, nelem);
      }
    }

Tree AllReduce

Tree AllReduce算法,是把AllReduce拆成了reduce和broadcast

假设各设备建立的梳妆拓扑结构如下:

在这里插入图片描述

Up:Reduce阶段, 从叶子节点开始,向根节点发送数据:

在这里插入图片描述

if (tree->down[0] == -1) {
        for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
          ssize_t offset = gridOffset + bid*int(chunkSize);
          int nelem = min(chunkSize, size-offset);
          prims.send(offset, nelem);
        }
      }

在这里插入图片描述
非根节点,向父节点发送数据,同时进行reduce

 for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
          ssize_t offset = gridOffset + bid*int(chunkSize);
          int nelem = min(chunkSize, size-offset);
          prims.recvReduceSend(offset, nelem);
        }

在这里插入图片描述
根节点接收数据,并执行reduce

 if (tree->up == -1) {
        for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
          ssize_t offset = gridOffset + bid*int(chunkSize);
          int nelem = min(chunkSize, size-offset);
          prims.recvReduceCopy(offset, offset, nelem, /*postOp=*/true);
        }
      }
if (tree->up == -1) {
        for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
          ssize_t offset = gridOffset + bid*int(chunkSize);
          int nelem = min(chunkSize, size-offset);
          prims.recvReduceCopy(offset, offset, nelem, /*postOp=*/true);
        }
      }
      else if (tree->down[0] == -1) {
        for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
          ssize_t offset = gridOffset + bid*int(chunkSize);
          int nelem = min(chunkSize, size-offset);
          prims.send(offset, nelem);
        }
      }
      else {
        for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
          ssize_t offset = gridOffset + bid*int(chunkSize);
          int nelem = min(chunkSize, size-offset);
          prims.recvReduceSend(offset, nelem);
        }
      }

down:broadcast 阶段,从根节点开始,向外广播

在这里插入图片描述

 if (tree->up == -1) {
        for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
          ssize_t offset = gridOffset + bid*int(chunkSize);
          int nelem = min(chunkSize, size-offset);
          prims.directSendFromOutput(offset, offset, nelem);
        }
      }

在这里插入图片描述

for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
          ssize_t offset = gridOffset + bid*int(chunkSize);
          int nelem = min(chunkSize, size-offset);
          prims.directRecvCopySend(offset, offset, nelem);
        }

在这里插入图片描述

else if (tree->down[0] == -1) {
        for (ssize_t gridOffset = 0; gridOffset < size; gridOffset += loopSize) {
          ssize_t offset = gridOffset + bid*int(chunkSize);
          int nelem = min(chunkSize, size-offset);
          prims.directRecv(offset, nelem);
        }
      }
<think>嗯,我现在需要了解NCCL的Reduce通信的执行步骤,并且要深入到源码层面。首先,我应该回忆一下NCCL是什么。NCCL是Nvidia Collective Communication Library,主要用于多GPU之间的高效通信,比如在深度学习训练中进行AllReduce、Broadcast等操作。Reduce操作是其中一种,它可能将多个GPU上的数据通过某种操作(比如求和)汇总到一个GPU上。 接下来,我需要理解Reduce的基本概念。比如,在MPI中,Reduce操作会将所有进程的数据通过某种操作合并到根进程中。NCCL的Reduce可能类似,但专门针对GPU优化。那么,NCCL的Reduce具体是如何实现的呢?执行步骤是怎样的? 首先,可能需要考虑数据在GPU间的传输方式。比如,使用PCIe或者NVLink这样的高速互联。然后,NCCL可能利用这些硬件特性来优化数据传输。但具体的执行步骤可能涉及到多个阶段,比如内存准备、通信协议的确定、数据传输、同步等。 接下来,我需要深入到源码层面。NCCL的源码是开源的,可以在GitHub上找到。比如,在源码中,Reduce相关的函数可能位于某个文件中,比如nccl/src/collectives/目录下可能有reduce的实现。例如,ncclReduce函数可能负责处理这个操作。 执行步骤可能包括:确定参与通信的GPU(即通信域),确定根节点,每个GPU准备发送缓冲区,根节点准备接收缓冲区。然后,根据不同的算法(比如环状算法、树状算法)来组织数据传输。比如,树状算法可能将数据逐层汇总到根节点,而环状算法可能通过循环的方式传递数据,逐步累积结果。 在源码中,可能看到对不同算法的选择,比如在某个函数中根据条件选择使用Ring还是Tree算法。例如,在ncclReduce函数中,可能会调用ncclTree或ncclRing相关的函数。每个算法都有不同的步骤,比如Ring算法可能需要将数据分成多个块,依次传递,每次传递时进行本地Reduce操作,直到所有数据被处理完。 另外,数据的分段传输可能也是一个优化点,比如将大的数据分成多个块并行传输,减少单次传输的开销。在源码中,可能会有循环处理每个数据块的代码,调用CUDA核函数来执行实际的Reduce操作(比如加法)。 还需要考虑同步机制,确保所有GPU在正确的时间点进行数据传输和计算。比如,使用CUDA事件或者流来同步各个GPU的操作,避免数据竞争或未完成的操作。 举个例子,假设使用Ring算法进行Reduce操作。步骤可能是这样的:每个GPU将数据分成若干块,然后依次向右的GPU发送一个块,同时接收左边GPU的块。每次接收到数据后,执行本地的Reduce操作(比如累加),然后将结果继续传递。经过若干次循环后,所有数据块都会被汇总到根GPU上。 在源码中,可能会看到类似这样的逻辑:确定环的拓扑结构,为每个GPU分配左右邻居,然后循环发送和接收数据块,调用CUDA核函数进行Reduce。每个数据块的处理可能通过一个循环,遍历所有参与Reduce的GPU,逐步累积结果。 此外,NCCL可能针对不同的硬件架构进行优化,比如针对Volta、Ampere架构的不同特性调整传输策略。例如,使用GPU的RDMA能力来加速数据传输,或者利用共享内存减少通信延迟。 在代码层面,可能需要查看具体的设备代码,比如在src/devices/目录下,不同GPU架构的实现可能有不同的优化。例如,ncclPrimitives结构体可能封装了发送和接收数据的函数,而针对Reduce操作可能会有专门的Kernel函数,使用CUDA C编写,高效地在GPU上执行数据合并。 总结起来,NCCL的Reduce执行步骤可能包括:初始化通信域和拓扑结构,选择最优算法(如Ring或Tree),分段数据传输,执行本地Reduce操作,同步等待所有传输完成。源码中的实现会涉及多个层次,从顶层的API调用到底层的CUDA核函数,以及不同硬件架构的优化代码。 不过,我可能需要具体查看源码来验证这些推测。例如,查找ncclReduce函数的实现,看看它调用了哪些内部函数,如何选择算法,以及数据传输的具体流程。此外,了解NCCL中的数据结构,如ncclComm结构体,可能存储了通信域的详细信息,如rank、节点数、拓扑结构等,这些信息在Reduce操作中会被使用到。 可能还需要注意,NCCL中的collectives操作通常由多个步骤组成,比如在Reduce中,可能需要将数据从各个rank发送到root,这可能涉及到多个步骤的协调和数据传输。在源码中,可能会有状态机或者类似的机制来管理这些步骤的执行顺序和同步。 最后,考虑到NCCL的高性能设计,可能还有诸如流水线传输、带宽优化、延迟隐藏等技术应用在Reduce操作中,这些都可能体现在源码的具体实现细节里。</think>NCCL(NVIDIA Collective Communication Library)中的 **Reduce** 操作用于将多个 GPU 上的数据按指定操作(如求和)汇总到目标 GPU(根节点)。其实现结合了硬件特性(如 NVLink、RDMA)和高效算法(如 Ring、Tree)。以下是其执行步骤及源码层面的关键分析: --- ### **1. 初始化通信域** - **通信域(ncclComm)**:存储 GPU 拓扑、Rank 信息、算法选择等。 - **源码关键结构**:`ncclComm`(定义于 `include/nccl.h`),包含成员如 `rank`(当前 GPU 的 ID)、`nRanks`(总 GPU 数)、`bootstrap`(通信初始化信息)等。 --- ### **2. 算法选择** NCCL 根据数据大小、GPU 拓扑动态选择算法: - **Ring 算法**:适合中等数据,通过环形通信逐段聚合。 - **Tree 算法**:适合大数据,分层聚合减少跳数。 **源码示例**(`src/collectives/collectives.cc`): ```cpp ncclResult_t ncclReduce(...) { // 选择算法 if (algorithm == NCCL_ALGO_AUTO) { if (bytes < 256 * 1024) algorithm = NCCL_ALGO_RING; else algorithm = NCCL_ALGO_TREE; } // 调用具体算法实现 if (algorithm == NCCL_ALGO_RING) { return ncclReduceRing(...); } else { return ncclReduceTree(...); } } ``` --- ### **3. Ring 算法实现** **步骤**: 1. **分块**:将数据分为 `chunkSize` 的块。 2. **环形传输**:每个 GPU 向右邻居发送数据块,同时接收左邻居数据块。 3. **本地 Reduce**:接收数据后执行本地操作(如累加)。 4. **循环迭代**:经过 `nRanks-1` 次迭代后,所有数据聚合到根节点。 **源码关键函数**: - `ncclReduceRing`(`src/collectives/ring.cc`): ```cpp ncclResult_t ncclReduceRing(...) { for (int step=0; step < nsteps; step++) { // 计算当前块的发送/接收地址 char* sendBuf = sendbuff + step * chunkSize; char* recvBuf = recvbuff + step * chunkSize; // 异步发送/接收 NCCL_CHECK(ncclTransportSend(...)); NCCL_CHECK(ncclTransportRecv(...)); // 本地 Reduce(CUDA 核函数) reduceKernel<<<blocks, threads>>>(recvBuf, sendBuf, count, op); } } ``` --- ### **4. Tree 算法实现** **步骤**: 1. **分层聚合**:构建二叉树,叶子节点向上层节点发送数据。 2. **递归 Reduce**:中间节点聚合子节点数据,直至根节点。 3. **带宽优化**:减少网络跳数,适合大规模数据。 **源码关键函数**: - `ncclReduceTree`(`src/collectives/tree.cc`): ```cpp ncclResult_t ncclReduceTree(...) { int parent = (rank - 1) / 2; // 父节点 int child1 = 2*rank + 1; // 子节点1 int child2 = 2*rank + 2; // 子节点2 // 接收子节点数据并聚合 if (child1 < nRanks) NCCL_CHECK(recvReduce(child1, ...)); if (child2 < nRanks) NCCL_CHECK(recvReduce(child2, ...)); // 发送到父节点 if (rank != root) NCCL_CHECK(send(parent, ...)); } ``` --- ### **5. CUDA 核函数优化** - **数据并行处理**:每个 CUDA 线程处理多个元素,隐藏内存延迟。 - **向量化访问**:通过 `uint4` 类型(128 位)合并内存操作。 **核函数示例**(`src/collectives/device/common_kernel.h`): ```cpp template<typename T, int UNROLL> __device__ void reduceLoop(...) { #pragma unroll for (int i=0; i < UNROLL; i++) { // 执行操作(如加法) if (op == ncclSum) res[i] += vals[i]; } } ``` --- ### **6. 硬件加速** - **RDMA 传输**:通过 GPU Direct 直接访问远程 GPU 内存。 - **NVLink 拓扑**:自动检测高速链路,优化数据传输路径。 **源码示例**(`src/devices/net.cc`): ```cpp ncclResult_t ncclTransportSend(...) { // 使用 RDMA 或 IPC 通道 if (use_rdma) { NCCL_CHECK(rdmaSend(...)); } else { NCCL_CHECK(ipcSend(...)); } } ``` --- ### **总结** NCCL 的 Reduce 操作通过算法优化(Ring/Tree)、硬件加速(NVLink/RDMA)和 CUDA 核函数高效执行数据聚合。其源码实现涵盖通信域管理、算法选择、数据传输和本地计算,充分适配现代多 GPU 系统的特性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值