RCCL · ROCm · 一个否定结论 · 实测于 2× RX 7900 XT

上报者自己的复现脚本只在 rank 0 上统计失败,
在两张卡上通过注入单边故障演示出来

用 rccl_allgather_truth.py 在两张 Radeon 上扫描通道与传输;对照跨 rank 变体与注入故障完成验证

上游 issue 报告两张 gfx1100 卡在集合通信时出现确定性静默数据损坏。在这一对 gfx1100 卡上,损坏并未出现。单凭这次干净的运行,几乎什么也证明不了。让它产生价值的,是三项独立于"多跑几遍"的具体检查。

2× RX 7900 XT · gfx1100 · VFIO 直通 RCCL 2.30.4 · ROCm 7.14 对照 2× W7800 裸机 实测 2026-08-27 与 2026-08-28

摘要

ROCm#6565 报告两张 W7800 上 all_gather 会静默返回损坏数据。上游报告将故障归结为初始化阶段的一次 8 字节拷贝:同步返回时,其载荷尚未写入设备内存。该问题在对方环境下的 RCCL 2.27.7 与 2.28.3 上均能确定性复现。但在运行 2.30.4 的这台机器上,八种配置下的 135 次冷通信器初始化均未出现损坏。

三项检查让这一否定结论提供了有效信息。其一,覆盖其机理所涉维度的参数扫描。所报故障仅波及多次并发初始化拷贝中的第一项;这台宿主机默认初始化两个通道,扫描将通道数从 1 扩展至 16,并将传输层从共享内存切换至 socket。其二,摆明两台测试机之间的差异。两台机器在平台、内核以及 RCCL 版本上各不相同,但在 PCIe atomics 与 IOMMU 上保持一致,因此那些显而易见的解释被逐一点明,无需读者猜测。其三,界定他人脚本跑出干净结果究竟能说明什么。

第三点恰恰是最关键的发现。上游复现脚本在所有 rank 上都计算了损坏校验,但仅在 rank 0 上统计失败计数;因此,仅在 rank 1 上出现的损坏依然会打印 ALL CORRECT。这一否定结论的实际覆盖范围因而比最初上报的更窄。该局限在当晚便已公开披露。随后我们编写了跨 rank 变体,并将全部八个 arm 重新测试了一遍。最终,这一盲点并未停留在对脚本逻辑的争论上,而是通过注入单边故障在这台硬件上实测演示了出来。

1为什么一次干净的运行通常什么也说明不了

"我这儿能跑"是缺陷追踪系统里最无价值的一句话。它可能意味着上报者出现了误判,可能意味着上报者测试了此处未涵盖的配置,也可能意味着测试框架根本无法捕捉该失效模式。要将否定结论转化为维护者可采信的证据,在提交之前必须回答三个问题:当前配置是否有能力触发该缺陷、两台机器之间究竟有何不同,以及测试工具本身存在怎样的观测盲区。

正是第一个问题确立了参数扫描的必要性。该故障机理具有高度特异性而非通用缺陷:通道 0 上最先发出的拷贝在两个 rank 上均告失败,而通道 1 上的拷贝则能正常写入设备内存。上报者指出,将相同的"分配 → memset → 8 字节异步拷贝 → 同步 → 读取"序列写成单设备测试,尝试 200 次也从未复现该故障。因此,并发的多设备初始化是触发缺陷的必要条件;若基准测试从未并发发起多次初始化拷贝,就无法测出任何相关信息。

图 1 · 八种配置,跑两遍 可由仓库重算
图 1. 每个 arm 都在一个全新的 torchrun 下跑完整的十二个用例, 所以每个计数都是那么多次冷通信器初始化,而不是一个进程里的重复 —— 他们那边是确定性失败,我们这边本可能是概率性的,而一次通过不构成"不存在"的证据。 统计数是从入库日志里反解出来的,不是从一个汇总文件里读的, 这样一份过期的汇总就没法和日志相矛盾的正文互相印证。两块面板是同样八个 arm、同样的次数, 分别在判定改成跨 rank 之前和之后。

通道扫描赋予了这一否定结论应有的证据效力。这台宿主机默认初始化两个通道,恰好吻合"首个拷贝失败而次个拷贝落盘"的拓扑形态。强制指定 1、4、8 与 16 个通道改变了并发初始化拷贝的数量,而禁用共享内存则强制底层传输走 socket 路径。在所有测试配置下,执行结果均完全正常,未见任何损坏。

2把差异摆明,而不是留给读者

否定结论只能隔离更大参数空间中的某一个特定格子。它的实际价值完全取决于能否精确标定该格子。系统之间相同的属性与相异之处同样重要:如果这台宿主机缺失 PCIe atomics,或者在没有 IOMMU 的环境下运行,维护者便能立即否定并忽略这一观测结果。

图 2 · 两台机器 从该目录的 README 提取
图 2. 从 benchmarks/rccl-6565/README.md 的表里提取, 这样文章不会和它背后的文档漂开。 atomics 那一行正是这个仓库出名的那条轴:自 2026-08-23 起这台 guest 已经广播 PCIe AtomicOp completer 支持,所以它不是一个缺 atomics 的格子,在这一点上与他们没有差别。 版本轴和平台轴没有被分开,而能分开它们的那两次运行在源文档里被点名, 不是含糊带过。

3先给界限,再做演示

最初的扫描直接逐字采用了 issue 追踪器中上报者的原版脚本,未经重新输入,并附带其 md5 校验和以供独立核验。直接运行未作修改的上游脚本确立了否定结论的可信度,但与此同时,也将其隐藏的缺陷一并带了进来。

rccl_allgather_truth.py · 它的形状
bad_list = [...]        # 每个 rank 都算
bad_into = [...]        # 每个 rank 都算
if rank == 0:           # 但只有 rank 0 计数、只有它打印
    if bad_list or bad_into:
        fail += 1

当数据损坏仅发生在 rank 1 时,脚本虽然检测到了异常数据,却将其直接丢弃,依然上报 ALL CORRECT。鉴于 135 次运行中的每一次通过均依赖该统计逻辑,这一否定结论实际上仅确立了"在 rank 0 上没有观察到损坏"。在上游 issue 发表评论约十小时后、且在外部提出任何质询之前,该局限便已被记录在代码仓库中。

直接修改上报者的脚本并非可行的解决方案。改动原始脚本(哪怕只是插入一次跨 rank 的规约)都会破坏其作为中立测试的地位。因此,原文件及其加密哈希被完整保留,同时实现了一份独立的变体脚本,并将两者并行对照测试。

图 3 · 一个注入的故障,三种判定 可用 scripts/blindspot_check.sh 自行核对
图 3. 在每个脚本的副本里,于校验之后插入一行, 在十二个用例中的一个上强制它在 rank 1、且只在 rank 1 上非空。 报告者的脚本判定这次运行正确并以 0 退出。变体把它计入并以 1 退出。 两个原件都没动过 —— 注入施加在副本上,而报告者的脚本仍然哈希成该目录引用的那个值。 这就是"说某个脚本有盲点"和"展示这个盲点会造成什么"之间的差别。

随后使用该变体对完全相同的八个 arm 及样本量进行了测试,在两个 rank 上均验证通过,录得 135 之 135 次干净执行。新的基准测试并未止步于细化警告说明,而是以实测数据消除了不确定性。两次扫描均经过校验,确保在完全匹配的样本次数下覆盖相同的配置,避免出现任何隐性偏差。

那个数错了的检查,以及它为什么留在这里 最初的运行框架使用 grep -c 统计上报 rank 的数量,然而该命令统计的是行数而非出现次数。由于两个 rank 频繁将输出打印在同一行中,测试框架在 20 次运行中将 6 次误报为单边上报,而实际上两个 rank 均已完成输出。由于脚本内部执行了 all-reduce,实际测量未受影响,但这一疏漏恰好映射了当前所调查的失效模式本身。一个会数错的检查比没有检查更糟。将其改为统计标识出现次数后,问题得以解决。

4那个没能起来的第二证人

验证方案原本规划了绕过 PyTorch 的第二证人,采用上报者提供的另一套复现工具。rccl-tests 套件基于容器工具链完成了干净编译,基准测试与验证二进制文件中均确认生成了 gfx1100 代码对象。然而,日志记录下的两次 all_gather 运行均在数据初始化内核处异常中止于 rank 1,报错为 HIP failure: 'invalid device function'。无论是否启用验证 pass,中止均以完全相同的方式发生,从而排除了验证内核的问题。测试脚本后续的几组从未执行,且代码树停留在针对 RCCL 2.30.4 的废弃分支上。

未过滤的原始日志被完整提交入库,将这一异常中止作为约束条件记录在案,而非直接丢弃。更早的一次尝试曾使用包装脚本解析控制台输出,但由于正则表达式未匹配到任何内容,导致每个集合通信都被误判为 FAILED,其中甚至包括在 PyTorch 框架下通过了 135 次的测试负载。该错误日志被果断废弃,因为残留一行错误的判定比完全没有日志更具误导性;取而代之的是真实的原始执行追踪日志。

5没有被确立的部分

版本轴对平台轴

当前宿主机在 RCCL 版本以及平台上均与对方不同,因此一次干净的运行无法单析任何单一因素。有两项测试本可解耦两轴,但在此处代价过高:上报者可在其机器上花数分钟由源码测试 2.30.4;而在本地部署 2.28.3 则需在仅剩 40 GB 空闲的空间下拉取 73 GB 镜像,并破坏既有容器状态。两项未做测试均在源文档中明确记述,以防后续重复排查。

135 次冷初始化框住了什么

本次干净运行仅确立了该缺陷并非 gfx1100 与 RCCL 组合下的普遍问题,且在当前硬件的 2.30.4 及所测通道与传输下均不复现。该结果完全无法约束上报者自有的软硬件配置,也无从约束:在其设备上,该故障是确定性发生的。

第二个证人

测试实际仅成功跑通了一个复现脚本。虽然这是上报者本人的主复现工具,也是最核心的一项验证,但若有一条独立的第二路径本会更具说服力,而这里的 rccl-tests 尝试未能提供该有效支持。

6此后发生的变化

2026-08-27 · 那条界限在当晚就披露了 上游第一条评论上报了 135 之 135 次通过,但未附加 rank-0 这一限定条件。该疏漏在大约十小时后被发现并提交记录至仓库,早于任何外部质询。
2026-08-28 · 界限被替换掉,而不是被重述 在实现跨 rank 变体后,全部八个 arm 在相同测试次数下完成重测,确认两个 rank 均完全正常。上游评论被同步修订,补充了跨 rank 数据列与注入检查,取代了原先未加限定的陈述。
仍然开着 ROCm#6565 依然处于开启状态,其他任何人均未针对 2.28.3 以上版本反馈任何方向的测试结果。本文仅提供一个对照格子,且仅作为单个对照点呈现。
2026-08-29 · "每一个集合通信"在已入库日志里只有一个 第 4 节最初表述为 rccl-tests 编译成功后"每一个集合通信都在 rank 1 上中止"。事实上,benchmarks/rccl-6565/logs/stage2b-raw.log 中仅包含两次调用,且均为 all_gather_perf;该目录下不存在任何 reduce_scatter_perf 或 all_reduce_perf 的输出记录。尽管 scripts/stage2b_rccltests.sh 原计划测试六组,但执行在第二组之后便告中断。脚本设计初衷不等同于已入库的实测证据,这正是本文着力厘清的核心界限。此外,该参数扫描原先被称作"本可以暴露故障的扫描";其实际覆盖的是其机理所指的维度,而故障注入演示的是检查工具本身,而非原发故障。

文中所用全部材料。所有测试脚本、变体实现、原始执行日志、故障注入追踪及硬件指纹均已归档于 benchmarks/rccl-6565/。图表数据由 verify_doc_figures.py 直接基于原始日志校验,同时确认两次扫描在相同测试次数下覆盖了完全一致的 arm。

相关结果已反馈至 ROCm#6565(上报者为 @BoJl4apa)。关于这台机器为何不缺失 atomics,参见另一篇文章。