返回博客

2026.11.03

跨卡拷贝 6 次全对,带宽 1.627 GB/s,显卡之间的直连一条都没通:怎么证明两张卡真的能直接读对方显存?

三张卡之间的显存直连还没解锁,我写的探针里跨卡拷贝 6/6 通过,带宽 1.627 GB/s,跟别人报的直连 1.68 只差 3%。拷贝能过,是因为驱动会悄悄绕道 CPU 内存中转;带宽分不出来,是因为两条路卡在同一根窄链路上。反方向也有人被骗:测出 6.7 GB/s,比链路物理上限高 3 倍多,实际一个字节都没搬。这篇给一个算链路上限的乘法、一个判断带宽有没有鉴别力的比值,和一个真正能判通断的探针写法。

GPUPCIe多卡测量方法硬件

我写了个探针,检查三张显卡两两之间的显存直连通没通。跨卡拷贝那一项,6 个方向 6 次全过,拷过去的数据一个比特都没错。

而那时候直连一条都没开。

更尴尬的是,我想用带宽把它揪出来,测出 1.627 GB/s。社区里有人在真打通直连的机器上测的是 1.68。差 3%,等于没差。

想直接拿判据的看第 4 章;想看带宽数字怎么从两个方向骗人,看第 2、3 章。

1. 没通的卡,交出了一张一半及格的答卷

先交代机器。三张挖矿版的数据中心卡,原厂 8GB,社区工具解锁到每张 64GB,每张只接了 PCIe 2.0 x4,窄得可怜。

显存直连(P2P)说白了就是:一张卡直接读写另一张卡的显存,不绕 CPU 那边的内存。多卡跑模型时,卡之间递数据要不要绕一圈,就看它。

社区里已经有人把这个功能也解开了。我打补丁之前想先想清楚一件事:打完之后,我拿什么判断它真的通了?

所以我反着来,先在没解锁的状态下把探针跑一遍,看看「没通」长什么样。结果是这样:

检查项 结果
驱动自报的能力位 全部 NO
开启对等访问 全部报「无效设备」
跨卡拷贝(6 个有向对) 6/6 通过
让 B 卡跑内核直接读 A 卡显存 0/6,非法地址
让 A 卡跑内核直接写 B 卡显存 0/6,非法地址

拷贝那一行差点骗了我。

原因说穿了很简单:拷贝接口发现两张卡之间没有直连,会合法地退回主机中转。先把数据从 A 搬回 CPU 内存,再从内存搬给 B。内容完全正确,返回值也是成功。站在它的角度,活确实干完了。

所以「拷贝成功」只证明数据到了,不证明数据走的是哪条路。

这一节我自己还错过一次。能力位那一栏,系统工具在三张卡两两之间都写着 GNS,我第一反应读成「硬件不支持」。错了。那是当前驱动报的能力位,解锁补丁要改的恰恰就是它。拿驱动自报的标签去判硬件,跟拿成功返回值去判路径,是同一种偷懒。

2. 我想拿带宽区分,结果是 1.627 对 1.68

我当时的想法很朴素:中转要搬两趟,总该慢一截吧。

测出来 1.627 GB/s。别人在打通直连的机器上报的是 1.68 GB/s。

先算这根链路本身能跑多快。公式就一个乘法:

链路单向上限 = 每条通道速率 × 通道数 × 编码效率 ÷ 8

PCIe 2.0 每条通道 5 GT/s,用 8b/10b 编码,每 10 位里有 8 位是有效数据,效率 0.8。代进去:

5 × 4 × 0.8 ÷ 8 = 2.0 GB/s
1.627 ÷ 2.0 = 81%
1.68  ÷ 2.0 = 84%

两个数都贴着天花板。那为什么分不出来?画一下两条路就明白了:

直连:A 的 x4 链路 → 主板转发 → B 的 x4 链路
中转:A 的 x4 链路 → CPU 内存 → B 的 x4 链路

两条路都得过 A 那根 x4 和 B 那根 x4。中间那段,CPU 内存比 2 GB/s 宽得多。只要驱动是分块流水搬的(前一块往内存回的同时,后一块已经在往 B 送),两趟能重叠,最后谁都卡在那根 x4 上。最窄的一段一样,结果就一样。

把它写成一个比值,就是带宽能不能用来区分两条路的判据:

鉴别比 = 中转路径上限 ÷ 直连路径上限
       = min(链路A, 主机内存, 链路B) ÷ min(链路A, 主板转发, 链路B)

这个比值离 1 越近,带宽越没用。我这台两边都卡在 2.0,比值约等于 1。实测 1.627 ÷ 1.68 = 0.968。

这里有两处我得自己先认:

  • 「驱动分块流水」是我从数字倒推的。如果它不流水、老老实实一趟搬完再搬下一趟,中转应该掉到 1.0 左右,实测没掉,所以我这么判断。驱动源码我没读。
  • 1.68 是别人的机器、别人的主板,我自己的 1.627 只测了一次,没测重复跑的波动有多大。在这种条件下,3% 的差距什么都说明不了。

3. 反方向的骗法:6.7 GB/s,一个字节都没搬

带宽在我这台上是「分不出来」。另一位社区作者碰到的是反过来的情况,而且狠得多。

他测的是上游一个分支走的另一条解锁路线。结果一片漂亮:能力位报可以,拓扑工具报 OK,单向带宽 6.7 GB/s,双向 13.4,延迟 1.7 到 1.9 微秒。

然后他去看内容:跨卡拷贝返回成功,目标显存里的值没变。多卡一起做求和的集合通信,在第一个 4 字节的操作上就挂死了。他把那份报告撤回了。

这个数其实拿第 2 章的乘法一算就该起疑:

6.7 ÷ 2.0 = 3.35 倍

测得的带宽超过链路的物理上限,数据就不可能真过了那根链路。这一条比任何复杂分析都快。

这条推论有个前提,我得标出来:它假设他那几张卡也跑在 Gen2 x4。这代卡的链路速率是出厂钳死在 Gen2 的,社区里有人用对照实验判过死;但他那台机器每个插槽的实际宽度,我没去单独确认。

还有一件事值得知道:显卡厂商自带的那个点对点带宽延迟测试,只计时,不校验内容。传对了和传了个寂寞,它给出的读数一样。

所以带宽在这件事上是两头骗:没通的时候,它跟通了一样(我这台);假通的时候,它比真通还好看(他那台)。

4. 真正能判通断的:让显卡自己去摸对方的显存

拷贝会绕路,带宽没鉴别力,那剩下的办法就是不给它绕路的机会。

做法是在 B 卡上跑一个极小的内核,直接按地址去读 A 卡显存里的一个值;再在 A 卡上跑一个内核,直接往 B 卡显存里写一个值。这一步没有中转可退。直连没开,结局只有一个:非法地址。通就是通,不通就是报错,没有中间地带。

光有这个还不够,几个细节决定它是不是一把真尺子:

每个有向对用不同的期望值。 三张卡有 6 个方向,每个方向跑 3 种检查,每次用的值都不一样。那份撤回报告里就出现过读到「本地别名」的现象:看起来读到了对方,其实读的是自己身上某块映射。值各不相同,这种碰巧就蒙不过去了。

目标先清零,再拷。 防止上一轮留下的值冒充这一轮的成功。

先跑负对照。 往一段显存里写一个值,确认读出来不等于哨兵值。这个对照很朴素,只能证明比较逻辑不会无条件返回「相等」。但我被永远报绿的探针坑过不止一次,这一步就留着了。比较逻辑要是连这关都过不去,整个探针直接中止,不给结果。

每个检查关进一个单独的子进程。 这是我踩出来的。非法地址这个错误在设备级是「粘」的:一旦踩了,同一个进程里后面所有调用都继续报错。我一开始写了个函数,想销毁上下文再重建来复位,结果连创建上下文那一步都报同一个错。18 个检查跑在一个进程里的话,第一个踩雷之后,剩下 17 个全被污染。放进子进程,脏了就跟着进程一起死,母进程只读退出码。

最后的结果签名是这样:

pairs=6 checks=18 failures=12

12 个失败正好是 6 次读加 6 次写,拷贝那 6 次都过了。没解锁的状态下,这就是正确答案。

这个签名可以提前算出来。n 张卡:

有向对数    = n × (n − 1)
检查总数    = 3 × n × (n − 1)
未解锁时的失败数 = 2 × n × (n − 1)

三张卡就是 6、18、12。跑出来跟预期对不上,先怀疑探针,别急着下结论。

判读规则也很直接:全过,说明真通;全挂(或者只有拷贝过),符合没解锁;部分通过最危险,意味着有几对卡会悄悄把数据传错,这种要当失败处理,并把具体哪几对列出来。

5. 我这边还没做完的

  • 探针只在锁定状态下跑过。 我最后没打补丁:按我这台机器的负载算了一笔账,换成要靠直连的切法是净亏的,不值得冒改驱动的风险。所以「全过 = 真通」这一侧,是别人的机器证明的;我这里只证明了「没通」能被正确报出来。
  • 1.627 只测了一次,没有波动数据。
  • 6.7 超上限的推论,依赖他那台卡也是 Gen2 x4。
  • 驱动分块流水那一句是从数字倒推的,没读源码。
  • 我这台三张卡分别挂在三个不同的上游桥下面,有一张挂在芯片组下;内核启动参数里还关掉了 BAR 重分配。这些在解锁之后会不会挡路,没测。
  • 集合通信那一步,也就是真让几张卡一起做一次最小的求和,我没跑。那份撤回报告就是死在这一步。

6. 照着做的顺序

  1. 先用乘法算链路单向上限:每条通道速率 × 通道数 × 编码效率 ÷ 8。测得带宽超过它,直接判数据没过那条链路,后面不用看了。
  2. 再画出直连和中转两条路,算鉴别比。接近 1,就别指望拿带宽区分它们。
  3. 驱动能力位、拓扑表只当参考。它们是驱动自报的,解锁改的正是它们。
  4. 拷贝接口返回成功,只说明数据到了,不说明路径。
  5. 判据用内核直接读写对方显存的指针,每个有向对一个不同的值,目标先清零。
  6. 跑之前先做负对照,尺子不过关就中止。
  7. 每个检查一个子进程,别让一次非法地址污染后面所有结果。
  8. 先按 n × (n − 1) 算出预期签名,再对照实际结果。
  9. 部分通过当失败处理,列出具体哪几对。
  10. 最后跑一次最小的集合通信求和,能跑完才算能用。