Shortcuts

NCCL性能调优

NCCL是Nvidia Collective multi-GPU Communication Library的简称,它是一个实现多GPU的协同通信(all-gather, reduce, broadcast)库,Nvidia做了很多优化,以在PCIe、Nvlink、InfiniBand上实现较高的通信速度。

Nvidia基于DGX平台对NCCL做了一系列优化,例如NCCL-2.4之后新增的双二叉树取代了原来的Ring All-Reduce作为ALL_REDUCE通讯的算法,在多节点延迟大幅降低的同时也显著提高了带宽。

但是,萤火超算集群DL训练节点跟DGX在硬件上存在差异,尤其是GPU通过PCIE直接与CPU连接,而非DGX那样采用NVLINK作为节点内互联,所以NCCL表现较差。为了保持最大兼容性,我们在不改动NCCL源码的情况下,根据我们机器的特点做了一些适应性优化,使NCCL的all_reduce性能得到60~130%的提升。

硬件架构剖析

对于我们的硬件架构,双路32核心AMD EPYC CPU搭配8张A100 GPU和1张200G IB卡,有如下的硬件拓扑:

nvidia-smi topo情况

节点内部互联示意图

可以看到除了GPU5和GPU6挂在同一个Pcie Host Bridge(Root Complex)下,其他GPU要么通过XGMI跨socket互联,要么经过cpu interconnect跨PHB连接(这种情况会使用CPU片上带宽)。 因此,对于默认情况下

NCCL_NET_GDR_LEVEL=not specified  
NCCL_NET_GDR_READ=0  

NCCL(2.9和2.12版本测试)是不会启用GPU Direct RDMA的。

在不开启GPU Direct RDMA时,nccl默认会使用CPU Send Proxy Thread 和 CPU Receive Proxy Thread来向其他节点发送和接受数据。如图:

这样一来,两个节点的Ring 就会形成以下拓扑:

other node->IB>CPU0->GPU0->GPU1->GPU2->GPU3->GPU4->GPU5->GPU6->GPU7->CPU1->IB->other node  

这样在Ring上会多出来两个CPU节点,增加了ring的长度,增大了延迟,当节点数量增大时,扩展性会严重下降。

开启GPU Direct RDMA之后:

IB(recv)->GPU0->GPU1->GPU2->GPU3->GPU4->GPU5->GPU6->GPU7->IB(send)  

Ring节点数量减少,延迟变低,且不会占用额外CPU资源,对系统影响小

NCCL拓扑图优化

考虑到我们的硬件结构,NCCL默认生成的 RING GRAPH TOPO 会产生跨socket的RDMA通讯,对于延迟敏感的场景,我们应当避免跨socket的RDMA操作。

default cross socket

如图,IB->GPU0这一步带来了跨socket的GPU RDMA操作,跨socket的高延迟会放大网络层面的短板效应。我们通过调整GRAPH TOPO,使与IB相同socket的GPU5和GPU4参与RDMA数据的收发,能很大程度减小rdma的延迟。同时跨socket的GPU7->GPU1以及GPU3->GPU4流量经过了不同的XGMI链路,尽量减小了出现带宽竞争的可能性。另外IB->socket0的流量也会经过不同的XGMI链路,这样就能保证NCCL通讯不会影响该节点IB跨socket的带宽。

optimized cross socket

IB(recv)->GPU5->GPU6->GPU7->GPU1->GPU0->GPU2->GPU3->GPU4->IB(send) 

另外需要注意的是,调优的拓扑56710234必须配合GPU Direct RDMA一起使用,如果不开GDR,如下图所示:

Heavy CPU1 without OPT GRAPH

socket1(CPU1)的负担会更重,部分场景会负优化

优化开启方法

使用示例

import hfai
hfai.distributed.set_nccl_opt_level(hfai.distributed.HFAI_NCCL_OPT_LEVEL.AUTO)
# user code...
# 下面是正常nccl创建dist以及通讯操作

也推荐使用hfai.distributed.set_nccl_opt_level(hfai.distributed.HFAI_NCCL_OPT_LEVEL.FULL) FULL等级优化,但需要注意目前已知跟sub group存在冲突。

接口说明:

入口函数 set_nccl_opt_level

set_nccl_opt_level( OPT_LEVEL, CUSTOM_CONFIG: optinal)

OPT_LEVEL 参数为HFAI_NCCL_OPT_LEVEL枚举类型,默认为0 (HFAI_NCCL_OPT_LEVEL.DISABLED),不开启优化

CUSTOM_FLAG 为 [ 可选 ] 参数:dict类型
{
  'GRAPH_OPT: 'path/to/your/graph.txt', #自定义GRAPH绝对路径路径
  'NCCL_ALGO': 'Ring',  # 设置NCCL拓扑算法  Ring/Tree
  'NCCL_PROTO': 'Simple', #设置NCCL通讯协议 LL/LL128/Simple 低延迟/128B低延迟/常规
  'GDR': True, # True/False 是否开启GPU Direct RDMA
  'MIN_NCHANNELS': '1',  
  'MAX_NCHANNELS': '1'
}

一般情况下,建议使用 OPT_LEVEL=HFAI_NCCL_OPT_LEVEL.AUTO或者HFAI_NCCL_OPT_LEVEL.FULL,不需要手动设置CUSTOM_FLAG。必要时通过 OPT_LEVEL=HFAI_NCCL_OPT_LEVEL.COVER_AUTO 覆盖AUTO中的配置参数

HFAI_NCCL_OPT_LEVEL 枚举类型:

class HFAI_NCCL_OPT_LEVEL(object):
    DISABLED = 0     # 禁用一切NCCL优化选项
    AUTO = 1         # 自动选择NCCL优化,保守策略
    FULL = 1         # 激进策略,开启全部最佳优化,已知跟sub group冲突
    COVER_AUTO = 10  # 在AUTO的基础上,用户自定义某些选项
    CUSTOM = 101     # 用户自行选择优化组合

目前只需要开启AUTO或者FULL等级的优化,函数库会根据机器判断是否开启优化,以及选择最适配的优化方法。如果需要手动修改,可以参考下面一个自定义优化配置的示例。

小参数调优示例

当需要进行大量<=1MB的小数据通讯时,建议手动开启LL128低延迟模式,避免NCCL自动判断没有使用LL128。AUTO模式NCCL会自动判断,非必要不开启此优化选项

import hfai
hfai.distributed.set_nccl_opt_level(OPT_LEVEL=hfai.distributed.HFAI_NCCL_OPT_LEVEL.COVER_AUTO, 
    CUSTOM_CONFIG={
    'NCCL_PROTO': 'LL128',
})

AUTO模式启用的优化选项:

os.environ['NCCL_NET_GDR_LEVEL'] = 'SYS'
os.environ['NCCL_NET_GDR_READ'] = '1'
os.environ['NCCL_MIN_NCHANNELS'] = '1'
os.environ['NCCL_MAX_NCHANNELS'] = '1'
os.environ['NCCL_ALGO'] = 'Ring'

FULL模式额外开启的优化选项:

os.environ['NCCL_GRAPH_FILE'] = os.path.join(os.path.dirname(os.path.realpath(__file__)), "graph1.txt")

检查环境是否能开启的逻辑:

  1. 1张ib卡

  2. 8张GPU

  3. GPU5/GPU6在同一个PHB下

  4. 跟IB卡不再同一个socket(numa node)的GPU数量至少在4张及以上

优化后性能对比

ALL_REDUCE性能

测试了1-64节点(共8-512卡)做all_reduce的性能,根据节点数量不同,相比优化前能取得68%~129%的速度提升:

A/B分组性能对比
A/B分组加速比

可以看到B分组MILAN节点性能原生比A分组的ROME节点更好,同时优化后MILAN节点优势更加明显(延迟更低)

Group A 加速情况
Group B 加速情况

由于Ring拓扑的特性,延迟会随着GPU数量增加而增加,所以扩展性呈现出稳定下降的趋势,所以建议当节点数量很多时(>30),可以考虑使用hfreduce

BERT训练性能

我们选用 bert 模型进行测试,测试源码在此,主要对比 hfreduce 与 NCCL 的性能表现(训练耗时)。 这里 hfreduce 用接口 hfai.nn.parallel.DistributedDataParallel 去实现,而 NCCL 用 Pytorch 中的 torch.nn.parallel.DistributedDataParallel 去实现,同时打开本文提到的set_nccl_opt_level(OPT_LEVEL=FULL)优化开关。

8机64卡BERT训练
64机512卡BERT训练

扩展性对比:

可以看到,在节点数量少时,优化后的NCCL能取得接近甚至超越hfreduce的训练速度;但在节点数量较多时,慢于hfreduce 20%,但相比原版NCCL依然有最多 40% 左右的提升。

FULL等级相比AUTO等级优化效果

FULL等级额外开启的GRAPH TOPO优化在节点数少时并不明显,但随着节点数量增加,效果也随之增加,相比只开启AUTO优化的训练,带来了额外 4.5% 的速度提升

附录

FULL等级优化调整后的NCCL_GRAPH_FILE拓扑:

<graphs version="1">
  <graph id="0" pattern="4" crossnic="0" nchannels="1" speedintra="24" speedinter="24" typeintra="SYS" typeinter="SYS" samechannels="1">
    <channel>
      <net dev="0"/>
      <gpu dev="5"/>
      <gpu dev="6"/>
      <gpu dev="7"/>
      <gpu dev="1"/>
      <gpu dev="0"/>
      <gpu dev="2"/>
      <gpu dev="3"/>
      <gpu dev="4"/>
      <net dev="0"/>
    </channel>
  </graph>
  <graph id="1" pattern="3" crossnic="0" nchannels="1" speedintra="12" speedinter="12" typeintra="SYS" typeinter="SYS" samechannels="1">
    <channel>
      <net dev="0"/>
      <gpu dev="5"/>
      <gpu dev="6"/>
      <gpu dev="7"/>
      <gpu dev="1"/>
      <gpu dev="0"/>
      <gpu dev="2"/>
      <gpu dev="3"/>
      <gpu dev="4"/>
      <net dev="0"/>
    </channel>
  </graph>
  <graph id="2" pattern="3" crossnic="0" nchannels="0" speedintra="0" speedinter="0" typeintra="NVL" typeinter="PIX" samechannels="1"/>
</graphs>