赞
踩
01
—
前言
深度学习模型通常使用 GPU 训练,因为 GPU 具有相比 CPU 更高的计算能力,以 Tesla V100 为例,使用 Tensor Core 加速的半精度浮点计算能力达到 125 TFLOPS【1】,配有 V100 GPU 的单个服务器节点最多可替代 60 个 CPU 节点,正如每年 GTC Keynote 上黄仁勋宣称的“The more you buy, the more you save”。
目前计算机视觉、语音识别、自然语言处理等领域最好的深度学习模型通常采用更高参数量模型结合更大规模数据训练得到。例如 ResNet-50 有大约 2300 万参数量,用于训练的 ImageNet 数据集有 128 万张图片,利用一块 Tesla V100 完成 90 轮(epoch)训练大约需要 2~3 天时间;中英文语音识别模型 DeepSpeech2 拥有 1 亿参数量,语音训练数据总时长超过 2 万小时,单 GPU 训练时间约 3~6 周;用于文本生成的 GPT-2 模型参数量多达 15 亿,预训练所需数据 WebText 多达 800 万篇文章,使用单块 GPU 训练耗时将长达一个月甚至更久。这些顶尖模型由于训练到部署迭代周期长,无法保证业务上线时间需求,为了加快节奏,需要借助更多 GPU 并行处理。
02
—
多 GPU 通信原理
2.1 单机多 GPU 通信
首先我们关注单台服务器上插有多张 GPU 的情况。受限于主板 PCIe 插槽数目和拓扑方式,在一台服务器中一般不超过 16 张卡。单机 8 GPU 或 16 GPU 已经可以承担一般规模训练任务。下图为典型的 8 卡 GPU 服务器外观:
8 卡服务器内部 CPU-CPU、CPU-GPU、GPU-GPU 之间互联拓扑关系如下图所示:
其中 CPU0 和 CPU1 通过 QPI 互联,CPU 与 GPU 则通过 PCIe Switch 互联。GPU 0~3 两两之间可以既可以通过 NVLink 通信又可以走 PCIe Switch,同样 GPU 4~7 也是如此。而 GPU 0 和 GPU 4 之间没有直接通路,需要借助其他途径间接通信。不同互联通路的有效带宽如下图所示:
可以看到两颗 GPU 之间如果有高速通路 NVLink,通信开销将远低于间接通路,从而实现线性扩展。程序设计时应尽可能减少使用低速通路如 QPI。
多 GPU 通信可以借助 CUDA API,通过显式调用 cudaSetDevice() 指定使用哪张卡进行显存分配和执行计算,如需交换数据,可以调用 cudaMemcpyPeer() 拷贝到目标设备,再启动 Kernel 计算。
除此之外,NVIDIA 提供了用于多 GPU 通信库 NCCL(NVIDIA Collective Communications Libary)【2】,实现了 AllReduce、Reduce、Broadcast、ReduceScatter、AllGather 等常用通信原语,面向 PCIe 和 NVLink 做了专门优化,具有更高带宽、更快速度。NCCL 最初只支持单机多 GPU 通信,从 NCCL2 开始支持多机多 GPU 通信。
后文几张图演示了 NCCL 各个原语的具体作用以及相应的 API 封装。约定每个 rank 只对应一块 GPU,rank 后面数字编号与 GPU 设备编号保持一一对应。
Broadcast
广播原语,将某个 rank 上的数据拷贝到其他所有 rank(图中示例将 rank 2 内容广播给 rank0~3)。
NCCL API Broadcast 调用接口如下:
ncclResult_t ncclBroadcast(const void* sendbuff, // 待广播数据地址 void* recvbuff, // 接收广播地址 size_t count, // 广播数据长度 ncclDataType_t datatype, // 广播数据类型,可选:ncclInt8, ncclChar, ncclUint8, ncclInt32, ncclInt, ncclUint32, ncclInt64, ncclUint64, ncclFloat16, ncclHalf, ncclFloat32, ncclFloat, ncclFloat64, ncclDouble int root, // 待广播设备编号 ncclComm_t comm, // NCCL 通信句柄 cudaStream_t stream) // 绑定的 CUDA 流
Reduce
归约原语,对所有 rank 数据做归约计算,结果放到其中一个 rank(图中示例将 rank0~3 数据归约后放到 rank2)。
NCCL API Reduce 调用接口如下:
ncclResult_t ncclReduce(const void* sendbuff, // 待归约数据地址 void* recvbuff, // 归约结果存放地址 size_t count, // 归约数据长度 ncclDataType_t datatype, // 归约数据类型,可选:ncclInt8, ncclChar, ncclUint8, ncclInt32, ncclInt, ncclUint32, ncclInt64, ncclUint64, ncclFloat16, ncclHalf, ncclFloat32, ncclFloat, ncclFloat64, ncclDouble ncclRedOp_t op, // 归约计算类型,可选:ncclSum(+), ncclProd(*), ncclMin, ncclMax int root, // 归约结果存放 rank ncclComm_t comm, // NCCL 通信句柄 cudaStream_t stream) // 绑定的 CUDA 流
AllReduce
完全归约原语,对所有 rank 数据做归约计算,结果放到每一个 rank。
注:AllReduce 等价于 Reduce+Broadcast。
NCCL API All Reduce 调用接口如下:
ncclResult_t ncclAllReduce(const void* sendbuff, // 待归约数据地址 void* recvbuff, // 归约结果存放地址 size_t count, // 归约数据长度 ncclDataType_t datatype, // 归约数据类型,可选:ncclInt8, ncclChar, ncclUint8, ncclInt32, ncclInt, ncclUint32, ncclInt64, ncclUint64, ncclFloat16, ncclHalf, ncclFloat32, ncclFloat, ncclFloat64, ncclDouble ncclRedOp_t op, // 归约计算类型,可选:ncclSum(+), ncclProd(*), ncclMin, ncclMax ncclComm_t comm, // NCCL 通信句柄 cudaStream_t stream) // 绑定的 CUDA 流
AllGather
完全汇聚原语,从 K 个 rank 各取长度为 N 的一段数据,结果按 rank 顺序汇聚到每个 rank,长度为 K*N。
NCCL API AllGather 调用接口如下:
ncclResult_t ncclAllGat
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。