跳到主要内容

8. MCCL加速GPU通信

8.1. MCCL简介

摩尔线程集合通信库 (MCCL) 可实现针对摩尔线程GPU 和网络进行性能优化的多 GPU 和多节点通信基元。MCCL 提供了 all-gather、all-reduce、broadcast、reduce、reduce-scatter、point-to-point send 和 receive 等原语,这些原语均经过优化,可通过节点内的 PCIe 和 MTLink 高速互联以及节点间的InfiniBand网络实现高带宽和低延迟。
MCCL支持节点内和跨节点通信。可以实现拓扑的自动检测,计算最佳的路径,最终实现GPUs之间的高效传输

img81

图1 MCCL框架图

8.1.1. 节点内

节点内 GPU 之间进行数据传输,主要的物理介质是 PCIe 和 MTLink,涉及到的主要技术有 GPUDirect P2P,GPUDirect P2P 避免数据通过 CPU 进行中转,降低延时,同时可以利用 GPU 高速互联的带宽。具体原理见下图

img82

图2 显示PCIe的P2P示意图

8.1.2. 跨节点

节点间 GPU 进行数数据,主要的物理介质有普通以太网或者 InfiniBand,涉及的技术有 GPUDirect RDMA,具体原理见下图
img83

图3 跨节点传输

8.2. MCCL支持的集合通信

8.2.1. Broadcast

广播操作将数据从一个 GPU (根 GPU) 分发到所有 GPU。 所有 GPU 的内容和跟 GPU 的内容一致。
img84

图4 broadcast运算示意图

8.2.2. Reduce

Reduce 把所有 GPU 的数据聚合到一个GPU,并对数据进行指定的规约操作。 所有GPU必须发送同等大小的数据。 此外,数据必须以能够执行 reduce 操作的方式进行对齐。 MCCL 支持四种操作: Sum, Product, Max, Min,Average。所有GPU规约计算的结果会压缩到跟GPU。
img85

图5 Reduce运算示意图

8.2.3. AllGather

All Gather 收集所有 GPU 的数据,并发送到所有 GPU。所有发送的数据块大小必须相同。目的GPU的数据由每个GPU组合而成。
img86

图6 AllGather运算示意图

8.2.4. AllReduce

All Reduce 的操作和 Reduce 一样, 但是需要向所有GPU发送相同的数据。
img87

图7 AllReduce运算示意图

8.2.5. Reduce Scatter

Reduce Scatter 操作和 All Reduce 类似,不同的是最终发送到所有GPU上的一列
img88

图8 Reduce Scatter运算示意图

8.3. MCCL的使用实例

8.3.1. 单线程多卡

下面的例子展示单线程使用 MCCL 进行多卡计算,将多卡的算力充分利用起来。

#include <stdio.h>
#include "musa_runtime.h"
#include "mccl.h"

#define MUSACHECK(cmd) do { \
musaError_t err = cmd; \
if (err != musaSuccess) { \
printf("Failed: Cuda error %s:%d '%s'\n", \
__FILE__,__LINE__,musaGetErrorString(err)); \
exit(EXIT_FAILURE); \
} \
} while(0)


#define MCCLCHECK(cmd) do { \
mcclResult_t res = cmd; \
if (res != mcclSuccess) { \
printf("Failed, MCCL error %s:%d '%s'\n", \
__FILE__,__LINE__,mcclGetErrorString(res)); \
exit(EXIT_FAILURE); \
} \
} while(0)


int main(int argc, char* argv[])
{
mcclComm_t comms[4];

//managing 4 devices
int nDev = 4;
int size = 32*1024*1024;
int devs[4] = { 0, 1, 2, 3 };


//allocating and initializing device buffers
float** sendbuff = (float**)malloc(nDev * sizeof(float*));
float** recvbuff = (float**)malloc(nDev * sizeof(float*));
musaStream_t* s = (musaStream_t*)malloc(sizeof(musaStream_t)*nDev);


for (int i = 0; i < nDev; ++i) {
MUSACHECK(musaSetDevice(i));
MUSACHECK(musaMalloc(sendbuff + i, size * sizeof(float)));
MUSACHECK(musaMalloc(recvbuff + i, size * sizeof(float)));
MUSACHECK(musaMemset(sendbuff[i], 1, size * sizeof(float)));
MUSACHECK(musaMemset(recvbuff[i], 0, size * sizeof(float)));
MUSACHECK(musaStreamCreate(s+i));
}


//initializing MCCL
MCCLCHECK(mcclCommInitAll(comms, nDev, devs));


//calling MCCL communication API. Group API is required when using
//multiple devices per thread
MCCLCHECK(mcclGroupStart());
for (int i = 0; i < nDev; ++i)
MCCLCHECK(mcclAllReduce((const void*)sendbuff[i], (void*)recvbuff[i], size, mcclFloat, mcclSum,
comms[i], s[i]));
MCCLCHECK(mcclGroupEnd());

//synchronizing on CUDA streams to wait for completion of MCCL operation
for (int i = 0; i < nDev; ++i) {
MUSACHECK(musaSetDevice(i));
MUSACHECK(musaStreamSynchronize(s[i]));
}

//free device buffers
for (int i = 0; i < nDev; ++i) {
MUSACHECK(musaSetDevice(i));
MUSACHECK(musaFree(sendbuff[i]));
MUSACHECK(musaFree(recvbuff[i]));
}

//finalizing MCCL
for(int i = 0; i < nDev; ++i)
mcclCommDestroy(comms[i]);

printf("Success \n");
return 0;
}

MCCL 还有其他的使用方式,如:多线程(每个 GPU 使用一个线程)和多进程模型(MPI 与 GPU 上的多线程操作相结合)。具体可以参考MCCL的文档。

8.4 FAQ

8.4.1 多卡编程时,多任务并行因 GPU 资源竞争,导致死锁,提示 GPU Overrun Error

错误描述:多个任务并行,每个任务同时使用多卡进行分布式计算或通信,且必须进行多卡间同步时,有可能发生因 GPU 计算资源竞争而导致的死锁。比如任务 1 和任务 2(可以是 process、thread、stream)都同时使用 GPU0-7 进行计算或通信,每个 Step 都需要进行一次 8 卡同步,则有可能出现以下死锁情景:

  • 任务 1 优先抢占到 GPU 0、1、2、3 资源,成功启动 Kernel1,但在 GPU 4、5、6、7 上,Kernel1 等待调度,同步被阻塞。
  • 任务 2 优先抢占到 GPU 4、5、6、7 资源,成功启动 Kernel2,但在 GPU 0、1、2、3 上,Kernel2 等待调度,同步被阻塞。

解决方法:应用侧保证各 GPU 上的调度时序,以确保任务 1 或任务 2 同时在所有 GPU 上被优先调度。具体如下:

  • 任务 1 及任务 2,在同一块 GPU 上,使用同一个 musa stream 以保证 kernel 调度串行化,同时所有 GPU 上,使用相同的调度时序,即 Kernel1 - Kernel2 或 Kernel2 - Kernel1。
  • 在不同任务间添加 barrier,确保任务间串行占用 GPU 资源,即任务 1 - 设备同步 - 任务 2。

img88