我的课程笔记,欢迎关注:https://github.com/BBuf/how-to-optim-algorithm-in-cuda/tree/master/cuda-mode
本篇文档的来源:https://github.com/stas00/ml-engineering 。这篇文档主要介绍了如何诊断和解决NCCL多GPU和多节点连接问题。它详细说明了如何使用NCCL调试信息来识别网络接口和协议问题,如何正确配置网络接口,以及如何在Docker容器中使用NCCL。文档还介绍了检查GPU P2P支持的方法,如何统计NCCL调用次数,以及一些有用的NCCL调试环境变量。此外,文档提供了一个用于测试分布式GPU设置的Python脚本。
网络调试
通常你不需要成为网络工程师就能解决网络问题。一些常见问题可以通过阅读以下注意事项来解决。
术语表
Bonding: 将多个网卡绑定在一起以获得更快的速度或作为备份
IB: InfiniBand(最初由Mellanox开发,后被NVIDIA收购)
如何诊断NCCL多GPU和多节点连接问题
本节肯定不是详尽无遗的,旨在涵盖我经常遇到的一些最常见的设置问题。对于更复杂的问题,请研究NCCL仓库的Issues(https://github.com/NVIDIA/nccl/issues),或者如果找不到与你情况匹配的问题,请提交一个新的Issue。NCCL还包括一个简短的故障排除部分(https://docs.nvidia.com/deeplearning/nccl/archives/nccl_2183/user-guide/docs/troubleshooting.html),但通常通过阅读Issues(https://github.com/NVIDIA/nccl/issues)可以学到更多。
对于网络诊断工作,我建议使用这个专门开发的设计测试脚本:
torch-distributed-gpu-test.py
(https://github.com/stas00/ml-engineering/blob/master/debug/torch-distributed-gpu-test.py),而不是使用可能需要很长时间启动并有不相关问题的完整应用程序。
首先,在设置以下环境变量后运行基于nccl的程序:
export NCCL_DEBUG=INFO
这将打印大量关于NCCL设置及其网络流量的调试信息。
例如,如果你使用上述调试脚本,对于一个有8个GPU的单节点,你可能会这样做:
NCCL_DEBUG=INFO python -m torch.distributed.run --nproc_per_node 8 --nnodes 1 torch-distributed-gpu-test.py
要在多个节点上启动它,你需要使用像SLURM或Kubernetes这样的编排软件,或者在每个节点上手动启动它(
pdsh
会非常有帮助) - 详细信息请参见
torch-distributed-gpu-test.py
(https://github.com/stas00/ml-engineering/blob/master/debug/torch-distributed-gpu-test.py)中的说明。但为了理解事物如何工作,我建议从1个节点开始,然后进展到2个节点,之后再到更多节点。
现在,检查程序的输出并查找以以下内容开头的行:
NCCL INFO NET/
然后检查它使用的是哪种协议和哪些接口。
例如,这样的输出:
NCCL INFO NET/FastSocket : Using [0]ibs108:10.0.19.12<0> [1]ibs109:10.0.19.13<0> [2]ibs110:10.0.19.14<0> [3]ibs111:10.0.19.15<0> [4]ibs112:10.0.19.16<0> [5]ibs113:10.0.19.17<0> [6]ibs114:10.0.19.18<0> [7]ibs115:10.0.19.19<0>
告诉我们使用了nccl-fastsocket(https://github.com/google/nccl-fastsocket)传输层插件,并发现了8个
ibs*
网络接口(NIC卡)。如果你使用的是Google Cloud,这是正确的,你的NCCL可能已经正确设置。但是如果你使用的是InfiniBand (IB)并得到上述输出,你可能会遇到非常低的节点间速度,因为这意味着你激活了错误的插件。
对于IB的情况,你希望看到的是
NET/IB
及其IB接口:
NCCL INFO NET/IB : Using [0]mlx5_0:1/IB [1]mlx5_1:1/IB [2]mlx5_2:1/IB [3]mlx5_3:1/IB [4]mlx5_4:1/IB [5]mlx5_5:1/IB [6]mlx5_6:1/IB [7]mlx5_7:1/IB [RO]; OOB eno1:101.262.0.9<0>
在这里,你可以看到IB被用于集体通信,使用了8个
mlx5_*
接口,还有一个OOB(代表带外通信)。OOB用于引导连接,通常使用较慢的以太网NIC(有时几个NIC会被绑定成一个(https://wiki.linuxfoundation.org/networking/bonding) - 如果你想知道接口名称中的
bond
是什么意思)。
要了解你的节点有哪些TCP/IP接口,你可以在其中一个节点上运行
ifconfig
命令(通常所有类似的节点都会有相同的接口名称,但并非总是如此)。
如果你的集群通信网络是IB,那么你应该运行
ibstat
而不是
ifconfig
。上面
NCCL INFO NET
的最后一个例子将对应以下输出:
$ ibstat | grep mlx5 CA 'mlx5_0' CA 'mlx5_1' CA 'mlx5_2' CA 'mlx5_3' CA 'mlx5_4' CA 'mlx5_5' CA 'mlx5_6' CA 'mlx5_7'
除了快速的节点间连接NIC之外,你很可能还有一个慢速的管理以太网NIC(甚至可能有几个),它用于配置节点、使用共享文件系统、访问互联网等。因此,
ifconfig
几乎肯定会包含额外的NIC。你也可能有一个docker网络接口、
lo
回环接口和其他一些接口。例如,在我的桌面上,我可能会得到以下输出:
$ ifconfig docker0: flags=4163 mtu 1500 inet 172.99.0.1 netmask 255.255.0.0 broadcast 172.99.255.255 inet6 f330::42:fe33:f335:7c94 prefixlen 64 scopeid 0x20 ether 02:42:fe:15:1c:94 txqueuelen 0 (Ethernet) RX packets 219909 bytes 650966314 (650.9 MB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 262998 bytes 20750134 (20.7 MB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73 mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10 loop txqueuelen 1000 (Local Loopback) RX packets 1147283113 bytes 138463231270 (138.4 GB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 1147283113 bytes 138463231270 (138.4 GB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 eth0: flags=4163 mtu 1500 inet 10.0.0.23 netmask 255.255.255.0 broadcast 10.0.0.255 inet6 2601:3108:1c71:600:4224:7e4b:13e4:7b54 prefixlen 64 scopeid 0x0 ether 04:41:1a:16:17:bd txqueuelen 1000 (Ethernet) RX packets 304675330 bytes 388788486256 (388.7 GB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 74956770 bytes 28501279127 (28.5 GB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 device memory 0xa3b00000-a3bfffff
我提到所有这些的原因是,关键部分是确保NCCL在其
Using
调试行中只报告正确的接口。如果最终报告了任何像
docker0
、
lo
或
eth0
这样的接口,例如:
NCCL INFO NET/Socket : Using [0]eth0:10.0.0.23<0>
如果你有更快的网络接口可用,这很可能不是你想要的。当然,在某些情况下,以太网NIC可能是你唯一的选择,那么上述情况就没问题 - 只是会非常慢。
有时,如果使用了错误的接口,应用程序可能会直接挂起。
如果你同时拥有正确和错误的接口,NCCL可能会工作,但速度会较慢。
如果是云环境,通常你的云服务提供商应该给你正确设置的说明。如果他们没有提供,那么你至少需要询问他们应该使用哪些网络接口来设置NCCL。
虽然NCCL会尽力自动发现应该使用哪些接口,但如果它无法正确做到这一点,你可以通过告诉它使用或不使用哪些接口来帮助它:
当不使用Infiniband时,可以使用
NCCL_SOCKET_IFNAME
来指定包含或排除哪些
ifconfig
接口。以下是一些例子:
export NCCL_SOCKET_IFNAME=eth: Use all interfaces starting with eth, e.g. eth0, eth1, …export NCCL_SOCKET_IFNAME==eth0: Use only interface eth0export NCCL_SOCKET_IFNAME==eth0,eth1: Use only interfaces eth0 and eth1export NCCL_SOCKET_IFNAME=^docker: Do not use any interface starting with dockerexport NCCL_SOCKET_IFNAME=^=docker0: Do not use interface docker0.
The full doc is here(https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html#nccl-socket-ifname).
When using IB RDMA (IB Verbs interfaces), instead of
NCCL_SOCKET_IFNAME
use
NCCL_IB_HCA
env var which selects the interfaces for the collective communications. Examples:
export NCCL_IB_HCA=mlx5 : # 使用所有以mlx5开头的卡的所有端口 export NCCL_IB_HCA==mlx5_0:1,mlx5_1:1 : # 使用mlx5_0和mlx5_1卡的1号端口 export NCCL_IB_HCA=^=mlx5_1,mlx5_4 : # 不使用mlx5_1和mlx5_4卡
完整文档可以在这里找到(https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html#nccl-ib-hca)。
例如,在使用IB时,通常会有一些额外的接口,如
mlx5_bond_0
,你不希望将其包含在NCCL通信中。例如,以下报告表明错误地包含了
[8]mlx5_bond_0:1/RoCE
接口,这几乎肯定会导致低带宽:
NCCL INFO NET/IB : Using [0]mlx5_0:1/IB [1]mlx5_1:1/IB [2]mlx5_2:1/IB [3]mlx5_3:1/IB [4]mlx5_4:1/IB [5]mlx5_5:1/IB [6]mlx5_6:1/IB [7]mlx5_7:1/I [8]mlx5_bond_0:1/RoCE [RO]; OOB ibp25s0:10.0.12.82<0>
在这种情况下,你可以通过以下方式排除它:
export NCCL_IB_HCA=^mlx5_bond_0:1
或者你也可以明确列出你想要使用的接口,例如:
export NCCL_IB_HCA==mlx5_0,mlx5_1,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_6,mlx5_7
如前所述,在使用IB互连的节点上使用
ibstat
命令将显示可用的IB接口。
由于NCCL会尝试自动选择最佳的网络接口,只有在NCCL不工作或运行缓慢时,你才需要进行上述操作。在正常情况下,NCCL应该可以开箱即用,用户无需进行任何特殊操作。
此外,根据使用的云服务不同,提供商很可能会给你一系列需要设置的环境变量。如果你错误地设置了其中一些变量,NCCL可能会运行缓慢或完全无法工作。
用户遇到的另一个典型问题是,当他们尝试在云B上重用在云A上正常工作的NCCL设置时。通常情况下,这些设置无法直接转换,需要仔细删除之前设置的所有环境变量,并为新的云环境重新正确设置。即使你使用的是同一个云服务提供商,但使用不同类型的实例,也可能出现这个问题,因为某些网络设置是针对特定实例的,在其他地方可能无法正常工作。
一旦你认为已经正确设置了NCCL,下一步就是对你的连接进行基准测试,确保它与宣传的速度相匹配(大约达到宣传速度的80%)。请前往基准测试章节(https://github.com/stas00/ml-engineering/tree/master/network/benchmarks)。
在Docker容器中使用NCCL
通过在docker
run
命令中添加以下额外参数来提供足够的资源:
–shm-size=1g –ulimit memlock=-1
(更多详情请参阅(https://docs.nvidia.com/deeplearning/nccl/archives/nccl_2183/user-guide/docs/troubleshooting.html#sharing-data))
特权访问:有时你需要在docker
run
参数中添加
--privileged
。
确保Docker镜像包含正确的软件包,例如,如果使用IB,你至少需要安装
libibverbs1 librdmacm1
如何检查是否支持P2P
有时你需要知道计算节点上的GPU是否支持P2P访问(Peer2Peer)。禁用P2P通常会导致节点内连接速度变慢。
你可以看到在这个特定的8个NVIDIA H100节点上,P2P是被支持的:
$ nvidia-smi topo -p2p r GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7 GPU0 X OK OK OK OK OK OK OK GPU1 OK X OK OK OK OK OK OK GPU2 OK OK X OK OK OK OK OK GPU3 OK OK OK X OK OK OK OK GPU4 OK OK OK OK X OK OK OK GPU5 OK OK OK OK OK X OK OK GPU6 OK OK OK OK OK OK X OK GPU7 OK OK OK OK OK OK OK X Legend: X = Self OK = Status Ok CNS = Chipset not supported GNS = GPU not supported TNS = Topology not supported NS = Not supported U = Unknown
另一方面,对于这个特定的2个NVIDIA L4 GPU,P2P是不被支持的:
$ nvidia-smi topo -p2p r GPU0 GPU1 GPU0 X CNS GPU1 CNS X
从图例中可以看出,
CNS
表示"芯片组不支持"。
如果你使用的是高端数据中心GPU,这种情况很少发生。不过,一些低端数据中心GPU可能不支持P2P,就像上面L4的例子。
对于消费级GPU,可能有多种原因导致你的GPU不被支持,通常是因为启用了IOMMU和/或ACS功能。有时只是驱动程序版本的问题。如果你花些时间搜索,可能会发现有人破解驱动程序以在不应支持P2P的GPU上启用P2P,比如这个4090 P2P支持仓库(https://github.com/tinygrad/open-gpu-kernel-modules)。
要检查是否启用了PCI访问控制服务(ACS)并禁用它们,请按照这个指南操作(https://docs.nvidia.com/deeplearning/nccl/archives/nccl_2183/user-guide/docs/troubleshooting.html#pci-access-control-services-acs)。
IOMMU可以在BIOS中禁用。
你还可以使用torch检查特定GPU之间的P2P支持 - 这里我们检查GPU 0和1:
python -c "import torch; print(torch.cuda.can_device_access_peer(torch.device('cuda:0'), torch.device('cuda:1')))"
如何统计NCCL调用次数
为子系统启用NCCL调试日志 - 集体通信操作:
export NCCL_DEBUG=INFOexport
NCCL_DEBUG_SUBSYS=COLL
如果你在一个有多个节点的Slurm环境中工作,你可能只想在rank 0上执行这个操作,像这样:
if [[ $SLURM_PROCID == "0" ]]; then export NCCL_DEBUG=INFO export NCCL_DEBUG_SUBSYS=COLLfi
假设你的所有日志都被发送到了
main_log.txt
,你可以通过以下方式统计每种集合通信调用的执行次数:
grep -a "NCCL INFO Broadcast" main_log.txt | wc -l 2590 grep -a "NCCL INFO AllReduce" main_log.txt | wc -l 5207 grep -a "NCCL INFO AllGather" main_log.txt | wc -l 1849749 grep -a "NCCL INFO ReduceScatter" main_log.txt | wc -l 82850
首先隔离训练的特定阶段可能是个好主意,因为加载和保存与训练迭代相比会有非常不同的模式。
所以我通常会先切片出一次迭代。例如,如果每次迭代的日志以
iteration: ...
开头,那么我会先执行以下操作:
csplit main_log.txt '/iteration: /' "{*}"
然后分析对应迭代的结果文件之一。默认情况下,它的名称会类似于
xx02
。
有用的 NCCL 调试环境变量
以下环境变量在调试 NCCL 相关问题(如挂起和崩溃)时最为有用。完整列表可以在这里找到。
NCCL_DEBUG
这是调试网络问题最常用的环境变量。
取值:
VERSION
- 在程序开始时打印 NCCL 版本。
WARN
- 当任何 NCCL 调用出错时打印明确的错误消息。
TRACE
- 在每次调用时打印可重放的跟踪信息。
例如:
NCCL_DEBUG=INFO python -m torch.distributed.run --nproc_per_node 2 --nnodes 1 torch-distributed-gpu-test.py
这将输出大量与NCCL相关的调试信息,如果发现报告了一些问题,你可以在网上搜索这些信息。
当使用
NCCL_DEBUG
时,
NCCL_DEBUG_FILE
应该非常有用,因为信息量很大,特别是在使用多个节点的情况下。