免责声明: 本文涉及对 GPU 驱动的修改,作者不对任何因此产生的后果负责。
PCIe P2P 访问是 GPU 之间进行通信的重要途径。它使得不同设备间的通信无需经过内存中转,直接通过 PCIe 总线传输,在很多时候能显著提升通信性能。当然,现实中的 P2P 性能也受到很多因素影响:比如 AMD EPYC 7002 家族的 PCIe Root Complex 转发性能就比较差;而如果主板上有 PCIe Switch 芯片,那么很多情况下几乎就能打满线速。但总体来说,P2P 的性能几乎总是比经过 GPU-内存-GPU 的路径要好。
然而众所周知,老黄精准的刀法砍掉了所有家用机显卡的 PCIe P2P 通信支持。换句话说:
harry@gpu:~$ nvidia-smi topo -p2p r
GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7
GPU0 X GNS GNS GNS GNS GNS GNS GNS
GPU1 GNS X GNS GNS GNS GNS GNS GNS
GPU2 GNS GNS X GNS GNS GNS GNS GNS
GPU3 GNS GNS GNS X GNS GNS GNS GNS
GPU4 GNS GNS GNS GNS X GNS GNS GNS
GPU5 GNS GNS GNS GNS GNS X GNS GNS
GPU6 GNS GNS GNS GNS GNS GNS X GNS
GPU7 GNS GNS GNS GNS GNS GNS GNS X
这里的 GNS 表示 GPU Not Supported。但真的是硬件不支持吗?似乎并不是这样。
前置工作
在计划使用 P2P 通信之前,首先要确认一些前置条件是否具备:
- 硬件拓扑:通常来说,同一个服务器 CPU Socket 上安装的 GPU 之间共享同一个 Root Complex,一般都能支持 P2P;跨 socket 通常也是可行的。而如果有 PCIe Switch,则可能有更复杂的拓扑(比如特定 GPU 之间有更高的带宽)。
nvidia-smi topo -m输出的结果很多时候未必准确:如在我的 AMD EPYC 9354 上,给出的所有结果都是SYS,或许和 NPS4 有关。 - BIOS 配置:关闭 PCIe ACS (Access Control Service),建议关闭 IOMMU 和虚拟化,前者亦可在内核选项中配置
iommu=pt绕过。这些功能虽然能提供更高的安全性,但也会让 PCIe 上的通信变得更复杂,很可能导致 P2P 性能急剧下降或者完全不可用。
魔改驱动
著名 iPhone 越狱黑客 Geohot 在几年前发起了一个精简的深度学习框架 tinygrad,力争通过最小的软件 overhead 实现深度学习框架。而这个框架的一个副产品就是,魔改过的 NVIDIA 的开源 GPU 驱动(tinygrad/open-gpu-kernel-modules),在不少家用级显卡上启用了 PCIe P2P 通信支持。所有的修改只有不到一百行的改动,大部分都是关掉判断,或者 patch 掉某些函数虚表,使用对应的数据中心卡的实现。所以可以推断,很大程度上,这些限制完全是来自于软件。真有你的,老黄。
虽说这个仓库的最新版本只到 565,另有高人搞出了基于 585 和 595 的版本(aikitoria/open-gpu-kernel-modules),顺便也就支持了 RTX 5090,其实核心改动基本还是差不多。
作者给出的安装脚本有些简单粗暴,直接当场构建模块并 insmod。这显然有点脏了。考虑到目前的模块都是 dkms 安装的,最好的办法其实是解包 NVIDIA 提供的 nvidia-kernel-open-dkms 包并替换文件。如果不打算频繁更新驱动的话,也可以采用一种略微少脏一些的办法:
# 安装官方驱动
apt install nvidia-driver-pinning-590.48.01
apt install nvidia-open=590.48.01-1 nvidia-kernel-open-dkms=590.48.01-1
# 卸载模块
rmmod nvidia_drm nvidia_modeset nvidia_uvm nvidia
# 下载魔改驱动
git clone [email protected]:aikitoria/open-gpu-kernel-modules.git
# 备份原有的驱动源代码
cd /usr/src/nvidia-590.48.01/
mkdir backup
mv kernel-open src backup/
# 打补丁
cd ~/open-gpu-kernel-modules/
cp -r kernel-open src /usr/src/nvidia-590.48.01/
# 重新构建驱动并安装
dkms build -m nvidia/590.48.01 --force
dkms install -m nvidia/590.48.01 --force
# 重新加载模块
modprobe nvidia
此时可以再次测试,是否已经化腐朽为神奇:
harry@gpu:~$ 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
性能测试
带宽延迟测试
CUDA Samples 中的 p2pBandwidthLatencyTest 可以测试 GPU 之间的通信带宽和延迟。启用 P2P 前:
Unidirectional P2P=Disabled Bandwidth Matrix (GB/s)
D\D 0 1 2 3 4 5 6 7
0 1528.86 31.57 31.12 30.91 28.82 29.39 29.51 29.86
1 30.66 1556.32 31.00 30.29 29.12 29.57 29.39 29.87
2 30.69 31.64 1556.27 30.29 31.22 31.26 31.37 31.14
3 31.12 31.58 30.96 1550.15 29.04 29.55 29.41 29.69
4 29.99 31.49 31.22 29.60 1553.28 31.22 31.44 31.13
5 30.18 31.72 31.42 29.80 30.61 1559.38 31.42 29.78
6 30.31 31.72 31.55 29.96 30.47 31.38 1559.38 30.73
7 30.03 31.39 31.04 29.63 30.83 31.05 31.41 1553.18
Bidirectional P2P=Disabled Bandwidth Matrix (GB/s)
D\D 0 1 2 3 4 5 6 7
0 1528.82 32.56 32.29 32.40 31.64 31.93 31.88 31.96
1 32.53 1540.88 32.61 32.53 31.77 32.06 32.08 31.68
2 32.28 32.52 1539.34 32.18 32.21 32.29 32.23 32.23
3 32.47 32.46 32.08 1540.88 31.50 31.80 31.89 31.83
4 31.54 32.03 32.36 31.41 1537.82 32.26 32.53 32.22
5 31.66 32.23 32.38 31.81 32.16 1539.36 32.52 32.07
6 31.80 32.19 32.53 31.78 32.34 32.65 1539.36 32.29
7 31.76 32.30 32.39 31.81 32.44 32.16 32.66 1540.88
P2P=Disabled Latency Matrix (us)
GPU 0 1 2 3 4 5 6 7
0 2.07 14.25 14.34 12.65 14.08 14.30 14.30 14.30
1 14.30 2.07 13.91 12.45 14.25 14.16 14.25 14.25
2 14.19 14.26 2.07 14.33 14.33 14.31 14.33 14.33
3 14.32 14.32 13.81 2.07 14.25 14.26 14.16 14.32
4 14.34 14.32 14.33 14.31 2.07 14.32 14.32 14.32
5 14.31 14.31 14.33 14.33 14.33 2.07 14.33 14.33
6 14.32 14.32 14.35 14.32 14.33 14.34 2.07 14.33
7 14.32 14.31 14.34 14.31 14.32 14.31 14.35 2.07
启用 P2P 后:
Unidirectional P2P=Enabled Bandwidth (P2P Writes) Matrix (GB/s)
D\D 0 1 2 3 4 5 6 7
0 1516.99 48.54 48.50 48.53 48.45 48.54 48.56 48.48
1 48.76 1543.97 48.64 48.61 48.62 48.58 48.62 48.57
2 48.75 48.74 1537.89 48.76 48.76 48.76 48.76 48.76
3 48.75 48.76 48.74 1543.97 48.76 48.74 48.76 48.74
4 48.75 48.76 48.76 48.76 1540.93 48.74 48.74 48.76
5 48.74 48.76 48.76 48.76 48.76 1547.03 48.76 48.74
6 48.73 48.65 48.64 48.64 48.64 48.65 1547.03 48.66
7 48.75 48.69 48.55 48.64 48.61 48.61 48.60 1544.02
Bidirectional P2P=Enabled Bandwidth Matrix (GB/s)
D\D 0 1 2 3 4 5 6 7
0 1530.32 97.42 97.44 97.46 97.41 97.45 97.44 97.44
1 97.43 1540.88 97.44 97.44 97.45 97.45 97.44 97.44
2 97.43 97.44 1539.34 97.44 97.45 97.44 97.44 97.45
3 97.44 97.44 97.41 1539.34 97.44 97.45 97.45 97.45
4 97.45 97.45 97.47 97.44 1537.82 97.45 97.46 97.47
5 97.46 97.45 97.45 97.44 97.44 1540.88 97.46 97.46
6 97.43 87.75 97.44 97.44 97.44 97.44 1540.86 97.45
7 97.44 97.45 97.45 97.45 97.46 97.44 97.44 1540.86
P2P=Enabled Latency (P2P Writes) Matrix (us)
GPU 0 1 2 3 4 5 6 7
0 2.07 0.37 0.35 0.37 0.43 0.37 0.36 0.36
1 0.36 2.07 0.35 0.37 0.36 0.43 0.43 0.42
2 0.38 0.43 2.07 0.38 0.38 0.44 0.37 0.38
3 0.38 0.43 0.37 2.07 0.36 0.36 0.37 0.37
4 0.45 0.37 0.44 0.38 2.07 0.45 0.37 0.43
5 0.36 0.35 0.36 0.36 0.37 2.07 0.43 0.35
6 0.43 0.42 0.43 0.36 0.36 0.43 2.07 0.43
7 0.36 0.44 0.36 0.36 0.36 0.36 0.36 2.07
也就是说,在启用 P2P 前后,各个指标都有了显著提升:
- 单向带宽:31.5 GB/s(由于需要经过内存,理论上限为 PCIe 5.0 x16 带宽的一半即 32 GB/s)-> 48.5 GB/s(理论上限为 64 GB/s)
- 双向带宽:32 GB/s(理论值为 64 GB/s)-> 97.4 GB/s(理论值为 128 GB/s)
- 通信延迟:14.3 us -> 0.4 us
集合通信测试
下面用 NCCL 来测试对多卡深度学习影响非常显著的 AllReduce 性能。
八卡启用 P2P 前:
# out-of-place in-place
# size count type redop root time algbw busbw #wrong time algbw busbw #wrong
# (B) (elements) (us) (GB/s) (GB/s) (us) (GB/s) (GB/s)
4096 1024 float sum -1 51.03 0.08 0.14 0 50.82 0.08 0.14 0
8192 2048 float sum -1 51.58 0.16 0.28 0 51.36 0.16 0.28 0
16384 4096 float sum -1 52.67 0.31 0.54 0 52.63 0.31 0.54 0
32768 8192 float sum -1 54.57 0.60 1.05 0 54.74 0.60 1.05 0
65536 16384 float sum -1 59.77 1.10 1.92 0 60.10 1.09 1.91 0
131072 32768 float sum -1 62.38 2.10 3.68 0 62.12 2.11 3.69 0
262144 65536 float sum -1 71.12 3.69 6.45 0 70.86 3.70 6.47 0
524288 131072 float sum -1 104.97 4.99 8.74 0 104.45 5.02 8.78 0
1048576 262144 float sum -1 163.53 6.41 11.22 0 161.47 6.49 11.36 0
2097152 524288 float sum -1 275.28 7.62 13.33 0 273.02 7.68 13.44 0
4194304 1048576 float sum -1 534.66 7.84 13.73 0 537.23 7.81 13.66 0
8388608 2097152 float sum -1 1015.74 8.26 14.45 0 1010.40 8.30 14.53 0
16777216 4194304 float sum -1 1999.61 8.39 14.68 0 2002.71 8.38 14.66 0
33554432 8388608 float sum -1 4011.43 8.36 14.64 0 4013.64 8.36 14.63 0
67108864 16777216 float sum -1 7972.72 8.42 14.73 0 7970.34 8.42 14.73 0
134217728 33554432 float sum -1 15721.4 8.54 14.94 0 15733.6 8.53 14.93 0
268435456 67108864 float sum -1 31422.7 8.54 14.95 0 31431.6 8.54 14.95 0
536870912 134217728 float sum -1 62893.2 8.54 14.94 0 62859.7 8.54 14.95 0
1073741824 268435456 float sum -1 128117 8.38 14.67 0 127351 8.43 14.75 0
由于 NCCL 默认并不愿意为 SYS 连接的 GPU 启用 P2P,因此需要通过 NCCL_P2P_LEVEL=SYS 强制打开。判断是否真的启用了 P2P 的方法是,设置 NCCL_DEBUG=INFO 后,查看日志中是否出现了类似的字样:
[0] NCCL INFO Channel 00/0 : 0[0] -> 1[1] via P2P/IPC
[5] NCCL INFO Channel 01/0 : 1[5] -> 0[4] via P2P/IPC
[1] NCCL INFO Channel 00/0 : 1[1] -> 0[0] via P2P/IPC
[0] NCCL INFO Channel 01/0 : 0[0] -> 1[1] via P2P/IPC
[4] NCCL INFO Channel 01/0 : 0[4] -> 1[5] via P2P/IPC
[1] NCCL INFO Channel 01/0 : 1[1] -> 0[0] via P2P/IPC
[7] NCCL INFO Channel 00/0 : 1[7] -> 0[6] via P2P/IPC
[7] NCCL INFO Channel 01/0 : 1[7] -> 0[6] via P2P/IPC
此时性能结果为:
# out-of-place in-place
# size count type redop root time algbw busbw #wrong time algbw busbw #wrong
# (B) (elements) (us) (GB/s) (GB/s) (us) (GB/s) (GB/s)
4096 1024 float sum -1 38.81 0.11 0.18 0 36.53 0.11 0.20 0
8192 2048 float sum -1 38.20 0.21 0.38 0 37.00 0.22 0.39 0
16384 4096 float sum -1 39.47 0.42 0.73 0 38.28 0.43 0.75 0
32768 8192 float sum -1 40.94 0.80 1.40 0 39.82 0.82 1.44 0
65536 16384 float sum -1 42.48 1.54 2.70 0 41.36 1.58 2.77 0
131072 32768 float sum -1 43.65 3.00 5.25 0 42.99 3.05 5.34 0
262144 65536 float sum -1 49.97 5.25 9.18 0 47.18 5.56 9.72 0
524288 131072 float sum -1 63.10 8.31 14.54 0 62.07 8.45 14.78 0
1048576 262144 float sum -1 84.45 12.42 21.73 0 85.24 12.30 21.53 0
2097152 524288 float sum -1 149.68 14.01 24.52 0 149.70 14.01 24.52 0
4194304 1048576 float sum -1 280.51 14.95 26.17 0 278.13 15.08 26.39 0
8388608 2097152 float sum -1 545.43 15.38 26.91 0 552.92 15.17 26.55 0
16777216 4194304 float sum -1 1091.57 15.37 26.90 0 1090.04 15.39 26.93 0
33554432 8388608 float sum -1 2169.79 15.46 27.06 0 2162.90 15.51 27.15 0
67108864 16777216 float sum -1 4336.85 15.47 27.08 0 4327.66 15.51 27.14 0
134217728 33554432 float sum -1 8640.99 15.53 27.18 0 8627.71 15.56 27.22 0
268435456 67108864 float sum -1 17226.8 15.58 27.27 0 17207.1 15.60 27.30 0
536870912 134217728 float sum -1 34359.1 15.63 27.34 0 34359.3 15.63 27.34 0
1073741824 268435456 float sum -1 68660.4 15.64 27.37 0 68652.1 15.64 27.37 0
也就是说,八卡 AllReduce 在 PCIe 上的带宽(busbw)从 14.75 GB/s 提升到了 27.34 GB/s。如果在一个 Socket 的四张 GPU 上测试,则提升会更明显,从 16.33 GB/s 提升到了 46.31 GB/s,大概是原来的 2.8 倍。
神秘修复
那么这些性能提升是完全免费的吗?是,也不是。我发现在魔改驱动后,一旦进程使用了 P2P 功能,那么有概率存在部分 GPU,在所有进程均退出后依旧维持 100% 的利用率和相当高的功耗。但只要再次在这些 GPU 上创建 CUDA Context,什么都不做并立刻销毁,就能使得它恢复正常静息状态。可以合理推测,魔改后的驱动应该存在一定的资源泄露问题。
为了缓解此问题,我在系统中增加了一个 systemd timer,定期检查是否有 GPU 无进程但高占用的情况,如果有则运行上述的简单修复程序。虽然方法比较土,但可以有效地缓解这个问题。
One More Thing
虽然目前的驱动已经支持 P2P,但是还是不能使用 GPUDirect RDMA 功能,而这是提升跨机通信性能的关键。既然 GPU 之间能互相通信,那么理论上 IB 卡和 GPU 通信应该也不困难。此外,从 580 开始, NVIDIA 驱动已经切换到了 Linux 官方支持的 dma-buf 来提供此支持,而不需要 nvidia-peermem 模块了。但很遗憾,这个魔改的驱动还没有打开此支持,而已经有人许下了良好的愿望。此外,GPUDirect Storage 也有着相同的原理,但它也还是一个愿望。
此外,近日我在小红书上还刷到了国内的神人通过 Claude 魔改驱动,还给 5090 增加了 NVLink 支持,甚至涉及到了关于 BAR 相关逻辑的修改。虽说确实 5090 的 PCB 上有此物理接口,但真的要支持并且用起来,又是另一个级别的工作了。如果此事真的可行,那只能说老黄的刀在当前的 vibe coding 时代,可能要再磨一磨了。