4、NCCL Benchmarks:NCCL-Tests编译与运行,理解allreduce、allgather、reduce_scatter等操作

聊到NCCL性能,光看文档可不行。你得亲手跑一跑,才知道你的集群到底几斤几两。NCCL官方提供了一套基准测试工具,叫NCCL-Tests。说白了,它就是一把尺子,专门用来量你的GPU之间通信到底有多快。

我个人习惯,拿到一个新集群,第一件事就是编译NCCL-Tests,跑一轮allreduce。为什么?因为allreduce是最常用的集合通信操作,它的吞吐基本决定了你分布式训练的上限。

4.1 编译NCCL-Tests

编译这东西其实不复杂,但有几个坑。我曾经在一个老旧的CentOS系统上折腾了半天,最后发现是CUDA路径没配对。

先拉代码:

git clone https://github.com/NVIDIA/nccl-tests.git
cd nccl-tests

然后编译。这里要注意,你得指定CUDA的安装路径。如果你机器上装了多个CUDA版本,千万别搞混了。

make CUDA_HOME=/usr/local/cuda-12.1 NCCL_HOME=/usr/local/nccl

嗯,这里有个小技巧。如果你用的是NVIDIA的官方容器,比如NGC的PyTorch镜像,里面已经预装了NCCL。这时候你直接make就行,它会自动检测到环境变量。

编译成功后,你会看到一堆可执行文件:

  • all_reduce_perf — 测allreduce的
  • all_gather_perf — 测allgather的
  • reduce_scatter_perf — 测reduce_scatter的
  • 还有broadcast、reduce等等,但最常用的就是上面三个
我的建议: 第一次编译时,最好用 make -j 并行编译,能省不少时间。但如果你内存不大,比如只有16GB,那还是老老实实单线程吧,不然容易OOM。

4.2 运行allreduce测试

编译完了,咱们先跑个最简单的allreduce。命令长这样:

./all_reduce_perf -b 8 -e 128M -f 2 -g 8

参数解释一下:

  • -b 8:起始数据量,8字节
  • -e 128M:结束数据量,128MB
  • -f 2:每次翻倍,也就是8、16、32...一直到128M
  • -g 8:用8张GPU

跑起来之后,你会看到类似这样的输出:

# nThread 1 nGpus 1 minBytes 8 maxBytes 134217728 step: 2(factor)
#
#     size       time   algbw   busbw
#      (B)    (usec)  (GB/s)  (GB/s)
       8       12.3    0.65    1.22
      16       12.5    1.28    2.40
      32       12.6    2.54    4.76
      ...
  134217728   1234.5  108.72  203.85

这里我重点说一下busbw这一列。它代表的是总线带宽,也就是实际有效带宽。你想想看,allreduce操作里,每个GPU既要发数据又要收数据,所以实际吞吐会比algbw高一些。我个人习惯,主要看busbw,因为它更接近硬件极限。

关键指标: 对于NVLink互联的8卡A100,busbw应该在600GB/s左右(取决于数据量大小)。如果你测出来只有200GB/s,那肯定有问题。

4.3 理解allgather和reduce_scatter

跑完allreduce,咱们再看看另外两个操作。它们仨其实是一家人,在Ring算法里经常配合使用。

allgather:每个GPU把自己的数据块广播给所有人。结果就是,每个GPU都拥有了完整的数据集。

./all_gather_perf -b 8 -e 128M -f 2 -g 8

reduce_scatter:反过来,每个GPU只保留一部分数据的规约结果。比如8张卡,每张卡只保留1/8的最终结果。

./reduce_scatter_perf -b 8 -e 128M -f 2 -g 8

你可能会问,这三个操作有啥关系?我画个图你就明白了。

NCCL集合通信核心操作关系图 AllReduce 每个GPU得到完整规约结果 AllGather 每个GPU得到完整数据集合 ReduceScatter 每个GPU只保留部分规约结果 AllReduce = ReduceScatter + AllGather 实际应用场景: • AllReduce:分布式SGD梯度同步(最常用) • AllGather:模型并行中的参数全量同步 • ReduceScatter:ZeRO优化器中的梯度分片规约 • 三者组合:Ring AllReduce = ReduceScatter + AllGather

你看,AllReduce其实可以拆成两步:先ReduceScatter,再AllGather。这个分解在Ring算法里特别有用。我当年调优的时候,就是通过分别测这两个子操作的性能,定位到了某个节点上的PCIe带宽瓶颈。

4.4 多节点测试

单机8卡测完了,咱们得看看多机的情况。多节点测试需要用到mpirun:

mpirun -np 16 -hostfile hostfile ./all_reduce_perf -b 8 -e 128M -f 2 -g 8

这里-np 16表示16个进程,每个节点8张卡,那就是2个节点。hostfile里写上节点IP或主机名。

注意: 多节点测试时,一定要确保所有节点上的NCCL版本一致。我曾经因为版本不一致,折腾了一整天,结果发现是不同节点上的NCCL对算法选择不一样,导致性能差了3倍。

4.5 常见问题与调优思路

跑完测试,你可能会遇到一些奇怪的现象。我列几个常见的:

现象 可能原因 解决思路
小数据量带宽低 通信开销占比大 尝试NCCL_ALGO=Ring,或者增大batch size
大数据量带宽上不去 NVLink或IB带宽瓶颈 检查拓扑,确认是否跨Socket
多节点性能差 网络拥塞或路由问题 设置NCCL_NET_GDR_LEVEL,尝试不同网卡绑定策略
结果不稳定 CPU频率波动或散热问题 锁定CPU频率,检查GPU温度

嗯,说到调优,我有个习惯。每次跑测试前,先看一眼nvidia-smi topo -m的输出,确认GPU拓扑。你想想看,如果两张卡不在同一个PCIe Switch下,它们之间的通信就要绕路,带宽自然上不去。

避坑指南: 我曾经在DGX-1上跑测试,发现8卡性能还不如4卡。查了半天,原来是NCCL默认用了P2P,但某些GPU之间没有NVLink直连,走了PCIe。后来设置NCCL_P2P_DISABLE=1强制走NVLink,问题就解决了。

最后,别忘了保存测试结果。我一般会把输出重定向到文件,方便后续对比:

./all_reduce_perf -b 8 -e 128M -f 2 -g 8 | tee allreduce_8gpu.log

这样,下次改了什么配置,一对比就知道效果如何。说白了,性能调优就是个不断测量、分析、调整的循环。NCCL-Tests就是你手里最趁手的工具。

想要更多资料,联系可可