ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

NCCL多卡通信性能测试实战:nccl-tests编译、单机与多机带宽排查指南

NCCL多卡通信性能测试实战:nccl-tests编译、单机与多机带宽排查指南

简介:nccl-tests 是一套面向 GPU 分布式通信开发与性能调优人员的 NCCL 测试工具集,基于 CUDA 环境构建,用于验证多卡、多进程乃至多节点场景下集合通信操作的正确性与性能表现。包内共 15 个文件,以 7 个 .cu 源文件为核心,覆盖 all_reduce、all_gather、reduce_scatter、broadcast、alltoall 等典型集合通信算子测试,另含 2 个头文件、2 个 Makefile、2 个 Markdown 文档及说明文本,压缩包约 27KB,结构紧凑、便于直接编译。构建时可通过 make 指定 CUDA_HOME 与 NCCL_HOME,若需多节点 MPI 支持,设置 MPI=1 与 MPI_HOME 即可完成带 MPI 的编译。测试支持多进程、多线程及每线程多 CUDA 设备运行,进程数由 MPI 控制。目前已有 4611 人学习下载,适合需要评估 NCCL 通信带宽、排查集合通信瓶颈或验证多卡并行正确性的开发者参考使用。

1. 从一次多卡训练掉速说起:nccl-tests 到底测什么

上个月帮一个做分布式训练的团队看问题,8 卡 A100 的机器,跑 ResNet-50 的 DDP 训练,单卡吞吐正常,一上多卡就掉到理论值的六成。日志里没有任何报错,loss 曲线也正常,就是慢。这种问题最难受——没有 crash,没有 OOM,你连从哪查都不知道。后来我让他们先别动训练代码,跑一遍 nccl-tests,五分钟就定位到是某两张卡之间的 P2P 带宽只有正常值的三分之一,根因是主板 PCIe 拓扑和 BIOS 里一个 ACS 选项没关。

这就是 nccl-tests 的价值:它不测你的模型,不测你的数据管道,只测 NCCL 这个集合通信库在你这台机器上到底能跑出多少带宽和延迟。NCCL 是 NVIDIA 多卡通信的底层,PyTorch 的 DDP、DeepSpeed、Megatron 全都建在它上面。训练掉速、多卡扩展比不上去、NCCL timeout,第一件事就该跑 nccl-tests 把硬件和拓扑的底摸清楚。它适合所有碰多卡的人——不管你是刚拿到机器要验收,还是训练跑着跑着怀疑通信有问题。下面按「编译 → 单机测 → 多机测 → 排错」这条线走一遍,参数怎么设、结果怎么看、坑在哪,都落到具体命令上。

2. 编译与拓扑确认:先把 NCCL 和 CUDA 的版本对齐

2.1 为什么编译这一步最容易翻车

nccl-tests 本身是一组 MPI 程序,它调用 NCCL 的 API 做 all-reduce、all-gather 这些集合操作,再用 CUDA event 计时。所以它同时依赖三样东西:CUDA Toolkit、NCCL 库、MPI 实现。这三者的版本必须和你的训练环境一致,否则测出来的数字没有参考价值——你测的是另一套运行时。

最常见的翻车是 NCCL 版本不匹配。系统里可能装了两个 NCCL:一个是 CUDA 自带的,一个是 conda 环境里的,还有一个是 PyTorch 自带的。编译 nccl-tests 时链接到哪个,运行时加载哪个,经常不是同一个。我一般会先确认训练用的 PyTorch 到底加载了哪个 NCCL:

# 看 PyTorch 实际加载的 NCCL 版本和路径 python -c "import torch; print(torch.cuda.nccl.version()); import torch._C; print(torch._C._nccl_version())" # 看动态链接库实际解析到哪个 libnccl ldd $(python -c "import torch; print(torch.__file__)" | xargs dirname)/lib/libtorch_cuda.so | grep nccl

第一行打印 NCCL 的版本号(一个整数,比如 21903 代表 2.19.3),第二行用 ldd 看 PyTorch 的 CUDA 库到底链到哪个 libnccl.so。如果这两个指向的路径和你编译 nccl-tests 时用的不是同一个,测出来的带宽就不能代表训练时的真实情况。这一步花两分钟,能省掉后面几小时的困惑。

2.2 编译命令与关键参数

确认版本后开始编译。nccl-tests 的 Makefile 支持通过环境变量指定 CUDA 和 NCCL 路径:

git clone https://github.com/NVIDIA/nccl-tests.git cd nccl-tests # CUDA_HOME 指向你的 CUDA Toolkit,NCCL_HOME 指向 NCCL 安装目录 make MPI=1 \ MPI_HOME=/usr/local/mpi \ CUDA_HOME=/usr/local/cuda-12.1 \ NCCL_HOME=/usr/local/nccl-2.19 \ NVCC_GENCODE="-gencode=arch=compute_80,code=sm_80" \ -j$(nproc)

MPI=1打开 MPI 支持,多机测试必须开。NVCC_GENCODE指定目标架构,A100 是 sm_80,H100 是 sm_90,写错了编译能过但跑起来会 JIT 重编译,第一次运行特别慢,容易误判成性能问题。NCCL_HOME一定要指向你训练环境实际用的那个 NCCL,不要图省事用系统默认的。

编译完在build/目录下会生成一堆可执行文件,常用的几个:all_reduce_perf、all_gather_perf、broadcast_perf、reduce_scatter_perf、sendrecv_perf。名字就是它测的集合操作,_perf后缀表示这是性能测试版本。

2.3 跑之前先确认拓扑

在测带宽之前,先搞清楚你的卡是怎么连的。NCCL 会自动探测拓扑并选择最优路径(NVLink、PCIe、还是跨 NUMA 走 QPI/UPI),但你需要知道它选了什么,才能判断结果是否合理:

# 看 GPU 拓扑矩阵,NV# 表示 NVLink,PIX/PXB 表示 PCIe 不同层级 nvidia-smi topo -m # 看 NCCL 自己探测到的拓扑和它选的算法 NCCL_DEBUG=INFO ./build/all_reduce_perf -b 8 -e 128M -f 2 -g 8 2>&1 | head -50

nvidia-smi topo -m输出的矩阵里,对角线是 X,两个 GPU 之间如果是NV18表示 18 条 NVLink 通道,PIX表示同一 PCIe switch 下,SYS表示跨 NUMA 节点。如果 8 卡机器上出现大量SYS,说明卡分属两个 CPU socket,跨 socket 通信会走 UPI,带宽直接砍半。NCCL_DEBUG=INFO会打印 NCCL 选了哪条路径、用了 ring 还是 tree 算法,这些信息在结果异常时是判断依据。

3. 单机测试:把 all_reduce 的带宽和延迟跑明白

3.1 核心命令与参数逐项拆解

单机测试是最常用的场景,一条命令就能跑出从 8 字节到 128MB 消息大小的完整带宽曲线:

# 8 卡单机 all_reduce,消息从 8B 到 128MB,每次翻倍,跑 20 次取平均 ./build/all_reduce_perf -b 8 -e 128M -f 2 -g 8 -n 20 -w 5 -c 1

参数含义逐个说清楚,这些参数直接决定结果可不可信:

  • -b 8:起始消息大小 8 字节,测的是小消息延迟。
  • -e 128M:结束消息大小 128MB,测的是大消息带宽。
  • -f 2:步进因子,每次消息大小乘 2,所以会测 8B、16B、32B……一直到 128MB。
  • -g 8:用 8 张 GPU。
  • -n 20:每个消息大小迭代 20 次。
  • -w 5:前 5 次作为 warmup 不计入结果。这个很关键,第一次跑会触发 JIT 编译和显存分配,不 warmup 的话小消息延迟会虚高。
  • -c 1:开启结果校验,每次 all_reduce 后检查数据正确性。调试阶段一定开着,性能测试时可以关掉减少干扰。

输出是一张表,每行一个消息大小,列包括time(耗时)、algbw(算法带宽)、busbw(总线带宽)。看结果主要看 busbw,不是 algbw。algbw 是消息大小除以时间,busbw 是 algbw 乘以一个和算法相关的系数(ring all-reduce 是 2*(n-1)/n),它反映的是实际物理链路上的流量。8 卡 A100 NVLink 全互联,busbw 峰值应该在 200 GB/s 以上;如果是 PCIe Gen4 的机器,峰值大概在 25-30 GB/s。低于这个数就说明有问题。

3.2 结果怎么读:三个典型异常

拿到结果不要只看峰值,要看曲线的形状。三种异常最常见:

小消息延迟高。8B 消息的 time 如果在 20 微秒以上,说明 kernel launch 开销或同步开销过大。常见原因是没 warmup、GPU 处于低功耗状态(nvidia-smi -q -d PERFORMANCE看 clocks throttle 原因),或者 NCCL 选了不合适的算法。可以试NCCL_ALGO=Ring强制指定算法对比。

大消息带宽上不去。128MB 的 busbw 如果只有理论值的一半,先查拓扑。用nvidia-smi topo -m确认是不是所有卡都在 NVLink 域内。如果是 PCIe 机器,检查NCCL_P2P_DISABLE和NCCL_SHM_DISABLE这两个环境变量有没有被意外设成 1——有些训练框架为了稳定性会关掉 P2P,代价就是带宽暴跌。

曲线中间有断崖。某个消息大小之后带宽突然掉一截,通常是 NCCL 在这个尺寸切换了算法或协议(从 LL 切到 LL128 再切到 Simple)。这是正常行为,但如果断崖位置和你的训练消息大小重合,就值得关注。可以用NCCL_DEBUG=INFO看切换点,再用NCCL_PROTO强制指定协议做对比。

3.3 多操作对比:别只测 all_reduce

DDP 训练里 all_reduce 是主力,但张量并行(TP)用 all_gather 和 reduce_scatter,流水线并行用 sendrecv。只测 all_reduce 可能漏掉问题:

# 依次测四种常用操作,消息范围一致便于对比 for op in all_reduce all_gather reduce_scatter broadcast; do echo "=== $op ===" ./build/${op}_perf -b 8 -e 128M -f 2 -g 8 -n 20 -w 5 -c 1 done

如果 all_reduce 正常但 all_gather 异常,问题可能在 NCCL 的某个特定 kernel 或你的拓扑对某种通信模式不友好。把四个结果放一起对比,能快速缩小范围。我一般会把输出重定向到文件,用脚本提取每个操作的峰值 busbw 做横向对比,比肉眼看表格快得多。

4. 多机测试:MPI 启动、网卡绑定与跨节点带宽

4.1 MPI 启动命令与 hostfile

多机测试的复杂度主要来自 MPI 配置和网络。先准备一个 hostfile,每行一个节点 IP 和槽位数:

# hostfile 内容示例,每台机器 8 个槽位 cat > hostfile <<EOF 192.168.1.10 slots=8 192.168.1.11 slots=8 EOF # 用 mpirun 启动,2 节点 16 卡 mpirun -np 16 \ --hostfile hostfile \ --allow-run-as-root \ -x NCCL_DEBUG=WARN \ -x NCCL_IB_HCA=mlx5_0,mlx5_1 \ -x NCCL_SOCKET_IFNAME=bond0 \ ./build/all_reduce_perf -b 8 -e 8G -f 2 -g 1 -n 20 -w 5 -c 1

注意-g 1:多机场景下每个 MPI rank 管一张卡,总卡数由-np决定。-x把环境变量透传给远程进程,这是多机调试的关键——你在本地 export 的变量不会自动传到远程节点。NCCL_IB_HCA指定用哪几张 RDMA 网卡,NCCL_SOCKET_IFNAME指定 TCP 通信走哪个网口。这两个设错了,NCCL 会 fallback 到 TCP socket,带宽直接掉一个数量级。

4.2 网络路径确认:IB 还是 RoCE

多机带宽上不去,九成是网络路径问题。先确认 NCCL 到底走了 RDMA 还是 TCP:

# 在 mpirun 里加 NCCL_DEBUG=INFO,抓取网络初始化日志 mpirun -np 16 --hostfile hostfile -x NCCL_DEBUG=INFO \ ./build/all_reduce_perf -b 8 -e 1G -f 2 -g 1 -n 10 -w 3 -c 1 2>&1 | \ grep -E "NET/IB|NET/Socket|Using network"

如果日志里出现NET/IB说明走了 RDMA,出现NET/Socket就是 TCP。RoCE 环境下还要确认 GID index 选对了(NCCL_IB_GID_INDEX),选错会导致 RDMA 建链失败然后静默 fallback 到 TCP。跨节点 all_reduce 的 busbw 在 100Gbps IB 上应该在 10-12 GB/s,200Gbps 上 22-25 GB/s,400Gbps 上 45-50 GB/s。低于这个范围先查网络,别怀疑 GPU。

4.3 跨节点测试的常见配置组合

多机测试的环境变量组合比较多,我整理了一张常用对照表,按场景查:

场景关键环境变量说明
InfiniBandNCCL_IB_HCA=mlx5_0指定 IB 网卡,多张用逗号分隔
RoCE v2NCCL_IB_GID_INDEX=3GID index 因驱动而异,需实测
多网卡聚合NCCL_IB_HCA=mlx5_0,mlx5_1让 NCCL 同时用多张卡
指定 TCP 网口NCCL_SOCKET_IFNAME=bond0避免走错网口导致降速
关闭 P2PNCCL_P2P_DISABLE=1仅调试用,会大幅降带宽
强制 RingNCCL_ALGO=Ring对比 Tree 算法差异

这张表里的值不是万能公式,RoCE 的 GID index 每台机器可能不同,用show_gids命令查实际值。多网卡场景下NCCL_IB_HCA写错一个字母,NCCL 不会报错,只会少用一张网卡,带宽减半但你不看日志根本发现不了。

5. 避坑与排查:那些让结果失真的细节

5.1 坑一:warmup 不够导致小消息延迟虚高

现象:8B 消息的 time 显示 50 微秒以上,但大消息带宽正常。原因:第一次执行 CUDA kernel 有 JIT 编译和 context 初始化开销,-w设太小(比如 0 或 1)时这些开销被计入平均。解决:-w至少设 5,小消息场景设 10。另外确认 GPU 不在低功耗状态,跑之前先用nvidia-smi -q -d PERFORMANCE看 clocks throttle 原因,如果是SW Power Cap或HW Slowdown,先解决散热和功耗墙。

5.2 坑二:NCCL 版本和训练环境不一致

现象:nccl-tests 跑出来带宽很好,但训练还是慢。原因:编译 nccl-tests 用的 NCCL 和 PyTorch 运行时加载的 NCCL 不是同一个版本,两者在算法选择和协议支持上有差异。解决:用 2.1 节的ldd方法确认 PyTorch 实际加载的 libnccl 路径,编译时NCCL_HOME指向同一个目录。如果 PyTorch 是 pip 装的,它自带的 NCCL 在torch/lib/下,直接指向那里。

5.3 坑三:MPI 环境变量没透传

现象:多机测试时本地设了NCCL_IB_HCA,但日志显示走了 TCP。原因:mpirun默认不把本地环境变量传到远程节点,必须用-x显式指定。解决:所有 NCCL 相关的环境变量都加-x。或者用--envlist批量指定。调试时加NCCL_DEBUG=INFO确认远程节点实际生效的配置。

5.4 坑四:GPU 被其他进程占用

现象:结果波动大,同一命令跑两次带宽差 30%。原因:机器上有其他训练任务或监控进程占着 GPU,显存和 SM 被分走。解决:跑之前nvidia-smi确认没有其他进程,必要时nvidia-smi --gpu-reset重置。容器环境里注意CUDA_VISIBLE_DEVICES的设置,别让 nccl-tests 看到不该看的卡。

5.5 坑五:消息大小范围设窄了

现象:只测了 1MB 到 128MB,结论是带宽正常,但训练还是慢。原因:训练中梯度 all_reduce 的消息大小可能只有几十 KB(小模型)或几 MB(大模型),如果只测大消息就漏掉了小消息区间的性能问题。解决:-b从 8 字节开始,-e至少覆盖到模型单次通信的最大消息。不确定的话直接-b 8 -e 1G,多花几分钟但覆盖全。

6. 进阶:用脚本把 nccl-tests 变成日常巡检工具

单次跑 nccl-tests 只能看当下状态,但硬件性能会漂移——网卡固件升级、驱动更新、BIOS 改配置、甚至机房温度变化都可能影响带宽。我现在的习惯是把 nccl-tests 包成一个巡检脚本,每次上机先跑一遍,和基线对比。

#!/bin/bash # nccl_health_check.sh - 多卡通信健康巡检 # 用法: ./nccl_health_check.sh <baseline_file> set -e NCCL_TESTS_DIR="/opt/nccl-tests/build" BASELINE="${1:-/opt/nccl-tests/baseline.txt}" RESULT="/tmp/nccl_result_$(date +%Y%m%d_%H%M%S).txt" # 固定参数,保证每次可比 COMMON_ARGS="-b 8 -e 256M -f 2 -g 8 -n 20 -w 10 -c 1" echo "=== all_reduce ===" | tee "$RESULT" $NCCL_TESTS_DIR/all_reduce_perf $COMMON_ARGS 2>&1 | tee -a "$RESULT" echo "=== all_gather ===" | tee -a "$RESULT" $NCCL_TESTS_DIR/all_gather_perf $COMMON_ARGS 2>&1 | tee -a "$RESULT" # 提取峰值 busbw 和基线对比 extract_peak() { grep -E "^\s+[0-9]+" "$1" | awk '{print $NF}' | sort -rn | head -1 } CURRENT_PEAK=$(extract_peak "$RESULT") BASELINE_PEAK=$(extract_peak "$BASELINE" 2>/dev/null || echo "0") echo "当前峰值 busbw: $CURRENT_PEAK GB/s" echo "基线峰值 busbw: $BASELINE_PEAK GB/s" # 低于基线 90% 就告警 if [ "$(echo "$CURRENT_PEAK < $BASELINE_PEAK * 0.9" | bc)" -eq 1 ]; then echo "警告: 带宽低于基线 90%,请检查拓扑和网络" exit 1 fi echo "通信健康检查通过"

这个脚本的关键点是参数固定。-b 8 -e 256M -f 2 -g 8 -n 20 -w 10这套参数每次都一样,结果才有可比性。extract_peak函数从输出里抓最大的 busbw 值,和基线文件对比,低于 90% 就退出码非零,方便接进监控系统。基线文件就是第一次跑出来的结果,存下来当参考。

用了一段时间后我发现,最有价值的不是单次结果,而是趋势。有一次巡检发现 all_reduce 峰值从 240 GB/s 慢慢掉到 210 GB/s,查了半天是某张卡的 NVLink 降速了,nvidia-smi -q看 NVLink 状态发现速率从 25 GT/s 掉到 20 GT/s,重启后恢复。这种渐进式劣化,不靠定期巡检根本发现不了。

从那以后我每次拿到新机器或者改过 BIOS、驱动、固件之后,都强制走一遍这个巡检脚本,把基线存下来。后面训练出问题,先跑巡检对比基线,五分钟就能排除或确认通信层的问题,不用再靠猜。希望这套流程能帮到你,少走一些我踩过的弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进