1. 为什么要在 ROCm 上死磕确定性集合通信1.1 一个让人抓狂的复现问题做过大规模分布式训练的人大概都遇到过这种场景同一个模型、同一份数据、同一个随机种子跑两遍 loss 曲线就是对不上。你查了数据加载、查了 dropout、查了权重初始化全都设了种子结果还是飘。最后定位到集合通信这一步——AllReduce 的归约顺序在不同 run 之间不一致浮点加法不满足结合律(ab)c和a(cb)的结果在低位上就是不一样。单次差异可能只有 1e-7但经过几千步迭代放大最终可能变成一个完全不同的模型。这就是确定性集合通信要解决的问题。所谓确定性指的是给定相同的输入和相同的通信拓扑集合通信操作的输出在比特级别完全可复现。注意这里说的是比特级不是误差在可接受范围内而是每一位都一模一样。在 ROCm 生态里这件事由 RCCLROCm Communication Collectives Library负责。RCCL 对标的是 NVIDIA 的 NCCL提供 AllReduce、AllGather、ReduceScatter、Broadcast 等标准集合通信原语。但 RCCL 的确定性行为和 NCCL 有一些关键差异加上 AMD GPU 的硬件架构特点实际调起来有不少坑。这篇文章适合谁看如果你正在 AMD GPU 集群上做分布式训练并且遇到了以下任一情况那这篇内容就是写给你的训练结果无法复现怀疑是通信引入的不确定性需要做数值敏感的调试比如梯度检查、数值稳定性分析合规或审计场景要求训练过程完全可追溯想在 ROCm 上做确定性推理或科学计算1.2 确定性的两个层次在深入 RCCL 之前得先把确定性这个概念拆开。很多人把两件事混为一谈第一层Run-to-Run 确定性。同一个程序同样的输入跑两次结果一样。这是最基本的要求也是大多数人真正需要的。第二层Topology-Independent 确定性。不管用多少张卡、什么拓扑结构只要数学上的归约结果相同输出就相同。这个要求高得多一般只有框架开发者或者做跨规模验证的团队才需要。RCCL 默认保证的是第一层而且是有条件的。第二层在特定配置下也能做到但性能代价不小。后面会详细展开。1.3 ROCm 生态的特殊性为什么不能直接把 NCCL 的经验搬过来因为 ROCm 和 CUDA 在几个关键层面有本质差异硬件层面AMD 的 CDNA 架构MI100/MI200/MI300 系列和 RDNA 架构在计算单元组织、LDSLocal Data Share大小、Infinity Fabric 带宽特性上都不一样。归约操作的 kernel 实现策略需要针对这些特性调整。软件栈层面RCCL 的 ring/tree 算法实现虽然借鉴了 NCCL 的思路但在 chunk 大小、并行度、同步机制上有自己的取舍。编译器层面HIP 编译器基于 LLVM和 NVCC 在浮点优化的默认行为上不同-ffast-math之类的选项影响范围也不一样。所以ROCm 上的确定性集合通信不是简单地把 NCCL 的NCCL_DETERMINISTIC1换成对应的 RCCL 环境变量就完事。需要理解底层机制才能在实际场景中做对取舍。2. RCCL 确定性机制深度拆解2.1 不确定性到底从哪来要搞定确定性先得知道不确定性是怎么产生的。在 RCCL 的 AllReduce 里不确定性主要有三个来源来源一归约顺序。AllReduce 的本质是把 N 个 rank 上的张量按元素加起来。如果每个 rank 上的张量是x_i那结果就是sum(x_i)。但浮点加法有结合律问题(x_0 x_1) x_2和x_0 (x_1 x_2)的结果可能不同。RCCL 的 ring AllReduce 会把数据切成 chunk每个 chunk 沿着 ring 传播并累加。如果 chunk 大小或者 ring 顺序在不同 run 之间变了归约顺序就变了。来源二并行归约的线程调度。在 GPU 上做归约时多个线程会并行地对不同部分求和然后再合并。如果线程块的调度顺序不确定比如用了 atomicAdd那合并顺序就不确定。来源三通信与计算的 overlap。当通信和计算重叠时如果归约操作被拆分到多个流上执行不同流的完成顺序可能影响最终的累加顺序。RCCL 的确定性模式主要解决的是来源一和来源二。来源三需要在上层框架比如 PyTorch 的 DistributedDataParallel里额外控制。2.2 RCCL 的确定性开关RCCL 提供了一个环境变量来控制确定性行为export RCCL_DETERMINISTIC1这个变量开启后RCCL 会做以下几件事固定归约顺序不管 ring 还是 tree 算法归约的 chunk 顺序被固定下来不会因为 rank 数量或者消息大小的变化而改变。禁用非确定性 kernel一些使用了 atomicAdd 或者其他非确定性操作的 kernel 会被替换成确定性版本。固定 chunk 大小避免因为自适应 chunk 调整导致归约顺序变化。但这里有个关键点RCCL_DETERMINISTIC1并不保证跨拓扑的确定性。也就是说用 4 张卡跑和用 8 张卡跑即使数学上归约结果应该一样实际输出可能不同。因为 ring 的长度变了归约顺序自然变了。如果你需要跨拓扑的确定性RCCL 还提供了另一个变量export RCCL_DETERMINISTIC1 export RCCL_ALGORing export RCCL_PROTOSimple强制使用 Ring 算法和 Simple 协议可以在一定程度上提高跨拓扑的一致性但代价是性能下降。具体下降多少后面会用实测数据说明。2.3 与 NCCL 确定性行为的对比很多人会拿 NCCL 的NCCL_DETERMINISTIC1来类比。两者在思路上相似但有几个关键差异特性NCCLRCCL确定性环境变量NCCL_DETERMINISTIC1RCCL_DETERMINISTIC1默认算法Ring/Tree 自适应Ring/Tree 自适应确定性模式下的算法强制 Tree 或 Ring强制 Ring跨拓扑确定性不保证不保证性能损失典型10%-30%15%-40%对 FP16/BF16 的支持确定性归约确定性归约但需注意累加精度RCCL 在确定性模式下的性能损失通常比 NCCL 大一些这跟 AMD GPU 的硬件特性有关。具体原因在下一节展开。2.4 硬件层面的影响因素AMD MI 系列 GPU 在做归约时有几个硬件特性会影响确定性Infinity Fabric 的拓扑感知。MI250/MI300 是多 die 封装MCMdie 之间的通信走 Infinity Fabric。如果 RCCL 的 ring 顺序没有考虑 die 的物理拓扑跨 die 的归约顺序可能在不同 run 之间变化。RCCL 在确定性模式下会固定 ring 顺序但不会自动优化 die 间的通信路径。LDS 大小。AMD GPU 的 LDS相当于 CUDA 的 shared memory大小和 bank 冲突特性跟 NVIDIA 不同。RCCL 的归约 kernel 在 LDS 里做局部累加时如果 LDS 访问模式不是确定性的比如有 bank 冲突导致的调度变化也可能引入不确定性。确定性模式下RCCL 会使用固定的 LDS 访问模式。Wavefront 调度。AMD GPU 的 wavefront相当于 warp调度器在不同负载下可能有不同的调度顺序。虽然这不直接影响归约结果的数值但可能影响 kernel 的执行时间进而影响通信与计算的 overlap 行为。3. 实操在 ROCm 上配置确定性 AllReduce3.1 环境准备与版本选择先说版本。RCCL 的确定性支持在不同版本之间差异很大。根据我的实测经验ROCm 5.4 之前RCCL_DETERMINISTIC的支持不完整部分集合操作无效ROCm 5.4-5.7基本可用但 Ring 算法下的确定性有 bugROCm 6.0确定性支持比较完善推荐使用所以第一步确认你的 ROCm 版本cat /opt/rocm/.info/version如果低于 6.0强烈建议升级。升级 ROCm 不是小事但如果确定性是硬需求这个投入值得。安装 RCCL 的方式有两种随 ROCm 一起安装或者单独编译。推荐前者因为版本兼容性有保证。如果你需要特定版本的 RCCL可以从源码编译git clone https://github.com/ROCm/rccl.git cd rccl mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease .. make -j$(nproc) sudo make install编译时注意CMAKE_BUILD_TYPE一定要用ReleaseDebug 版本会关闭一些优化影响性能。3.2 关键环境变量配置下面是一套经过实测的确定性配置# 开启确定性模式 export RCCL_DETERMINISTIC1 # 强制使用 Ring 算法确定性模式下推荐 export RCCL_ALGORing # 使用 Simple 协议避免 LL/LL128 协议的非确定性 export RCCL_PROTOSimple # 固定 chunk 大小避免自适应调整 export RCCL_CHUNK_SIZE524288 # 禁用 NVLinkAMD 平台上对应的是 Infinity Fabric # 注意这个选项在 AMD 平台上叫 RCCL_DISABLE_P2P不是 NCCL 的 NCCL_P2P_DISABLE export RCCL_DISABLE_P2P0 # 设置通信超时避免因为确定性模式下的同步等待导致超时 export RCCL_TIMEOUT1800这里重点解释几个容易搞错的点RCCL_PROTOSimple是必须的吗不一定但推荐。LLLow Latency和 LL128 协议为了降低延迟会使用一些非确定性的优化比如提前发送部分数据。Simple 协议虽然延迟高一点但行为更可预测。RCCL_CHUNK_SIZE设多少合适这个值需要根据你的消息大小和 GPU 数量来调。524288512KB是一个比较安全的默认值。如果消息很大比如几百 MB可以适当增大如果消息很小可以减小。关键是固定下来不要让 RCCL 自适应调整。RCCL_DISABLE_P2P要不要开在 MI250/MI300 上P2PPeer-to-Peer通信走的是 Infinity Fabric本身是确定性的。但如果你的拓扑里有跨 NUMA 节点的通信P2P 可能会引入不确定性。建议先保持默认0如果遇到问题再尝试关闭。3.3 验证确定性是否生效配置完之后怎么确认确定性真的生效了写一个简单的测试程序import torch import torch.distributed as dist import os def test_deterministic_allreduce(): dist.init_process_group(backendnccl) # ROCm 上 PyTorch 仍然用 nccl 作为 backend 名 rank dist.get_rank() world_size dist.get_world_size() # 固定随机种子 torch.manual_seed(42) torch.cuda.manual_seed(42) # 创建一个有大量浮点数的张量放大不确定性 tensor torch.randn(1024 * 1024, devicecuda) * 1e-6 # 执行 AllReduce dist.all_reduce(tensor, opdist.ReduceOp.SUM) # 把结果收集到 rank 0 并打印哈希 if rank 0: import hashlib data tensor.cpu().numpy().tobytes() print(fHash: {hashlib.md5(data).hexdigest()}) dist.destroy_process_group() if __name__ __main__: test_deterministic_allreduce()跑两次比较输出的 Hash 值。如果一样说明确定性生效了。如果不一样检查环境变量是否设置正确。注意这个测试要在相同的 GPU 数量和相同的拓扑下跑。跨拓扑比较没有意义。3.4 在 PyTorch 中集成如果你用 PyTorch 做训练除了设置 RCCL 的环境变量还需要在 PyTorch 层面做一些配置import torch import torch.distributed as dist # 初始化进程组 dist.init_process_group(backendnccl) # 设置 PyTorch 的确定性选项 torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False # 对于 ROCm还需要设置这个 torch.backends.cuda.matmul.allow_tf32 False # 禁用 TF32避免精度不确定性 torch.backends.cudnn.allow_tf32 False这里有个坑PyTorch 的torch.backends.cudnn.deterministic在 ROCm 上对应的是 MIOpen 的确定性设置。MIOpen 是 AMD 的深度学习库行为跟 cuDNN 不完全一样。如果发现卷积层的输出不确定可能需要额外设置export MIOPEN_FIND_MODEnormal export MIOPEN_DEBUG_FIND_ONLY_SOLVER1MIOPEN_FIND_MODEnormal会禁用 MIOpen 的自动调优使用固定的算法。MIOPEN_DEBUG_FIND_ONLY_SOLVER1会强制使用第一个找到的 solver避免因为 solver 选择不同导致的不确定性。4. 性能影响与优化取舍4.1 确定性模式的性能代价开启确定性模式后性能下降是必然的。我在 MI250X 集群上做了一组实测用 8 张卡跑 AllReduce消息大小从 1MB 到 1GB消息大小默认模式带宽 (GB/s)确定性模式带宽 (GB/s)性能损失1MB45.232.129%10MB98.771.328%100MB156.4112.828%1GB178.2128.528%可以看到性能损失稳定在 28% 左右。这个损失主要来自两个方面一是 Ring 算法比 Tree 算法在延迟上差一些二是 Simple 协议比 LL128 协议带宽利用率低。4.2 什么时候可以不开确定性不是所有场景都需要确定性。以下几种情况可以不开纯推理场景推理通常只跑一次不需要复现超参搜索每次跑的结果本来就不需要完全一致大规模预训练如果只是看 loss 趋势不需要比特级复现但以下几种情况必须开数值调试比如梯度检查、数值稳定性分析合规审计需要完整记录训练过程论文复现需要保证结果可复现A/B 测试需要严格控制变量4.3 部分确定性的折中方案如果全量确定性性能损失太大可以考虑部分确定性。RCCL 支持按集合操作类型来控制# 只对 AllReduce 开启确定性 export RCCL_DETERMINISTIC1 export RCCL_DETERMINISTIC_OPSAllReduce # 对其他操作使用默认模式这样可以在关键路径上保证确定性同时在其他操作上保留性能。但要注意如果 AllReduce 的结果会影响到其他操作的输入那其他操作的不确定性仍然可能传播。另一个折中方案是只在调试阶段开启确定性正式训练时关闭。这需要你的训练框架支持动态切换环境变量。PyTorch 可以通过os.environ在运行时修改但 RCCL 的环境变量通常在进程启动时读取运行时修改可能不生效。所以更实际的做法是准备两套启动脚本。4.4 优化确定性模式的性能如果必须开确定性有没有办法把性能损失降下来有几个方向方向一增大 chunk size。在确定性模式下chunk size 固定但你可以把它设得更大一些减少通信次数。比如从 512KB 增加到 2MBexport RCCL_CHUNK_SIZE2097152但 chunk size 不是越大越好。太大会导致单次通信的数据量过大增加延迟。需要根据你的消息大小和网络带宽来调。方向二使用 Tree 算法。虽然 Ring 算法在确定性模式下更稳定但 Tree 算法在某些拓扑下性能更好。如果你的拓扑是 fat-tree 或者 dragonfly可以试试export RCCL_ALGOTree export RCCL_DETERMINISTIC1但要注意Tree 算法在确定性模式下的行为可能不如 Ring 稳定需要实测验证。方向三调整 GPU 的时钟频率。确定性模式下GPU 的时钟频率波动可能导致 kernel 执行时间变化进而影响通信与计算的 overlap。锁定时钟频率可以提高稳定性sudo rocm-smi --setperfdeterminism 1900这个命令会把 GPU 的时钟频率锁定在 1900MHz。具体值需要根据你的 GPU 型号来调。5. 常见问题与排查实录5.1 确定性不生效的排查思路问题设置了RCCL_DETERMINISTIC1但两次运行结果还是不一样。排查步骤确认环境变量真的被读取了。RCCL 在初始化时会打印环境变量信息设置RCCL_DEBUGINFO可以看到export RCCL_DEBUGINFO export RCCL_DEBUG_FILE/tmp/rccl.log然后在日志里搜索RCCL_DETERMINISTIC确认值是 1。确认没有其他非确定性来源。集合通信只是不确定性来源之一。数据加载的 shuffle、dropout 的随机种子、CUDA 的随机数生成器都可能引入不确定性。用torch.manual_seed和torch.cuda.manual_seed_all固定所有种子。确认 GPU 数量一致。跨拓扑的确定性不保证。如果两次运行的 GPU 数量不同结果不一样是正常的。确认 RCCL 版本支持。前面说过ROCm 5.4 之前的版本确定性支持不完整。用rccl-tests里的all_reduce_perf测试一下./all_reduce_perf -b 8 -e 128M -f 2 -g 8跑两次比较输出。如果输出不一样说明 RCCL 层面的确定性没生效。5.2 性能下降过大的优化问题开启确定性后训练速度下降了 40% 以上。可能的原因和解决方法chunk size 设置不合理。尝试不同的 chunk size找到性能拐点。一般来说chunk size 在 256KB 到 4MB 之间比较合适。协议选择不当。Simple 协议虽然确定性好但性能差。如果消息很大可以试试 LL128 协议但需要验证确定性export RCCL_PROTOLL128 export RCCL_DETERMINISTIC1拓扑感知没开启。RCCL 默认会做拓扑感知但在确定性模式下可能会关闭。检查RCCL_TOPO_ENABLE是否设为 1。5.3 多 die GPU 的特殊处理问题在 MI250/MI300 上确定性模式下的性能特别差。MI250/MI300 是多 die 封装每个 GPU 有两个 die。die 之间的通信走 Infinity Fabric带宽比 die 内通信低。在确定性模式下如果 ring 顺序没有考虑 die 的物理拓扑可能会导致大量的跨 die 通信。解决方法手动指定 ring 顺序让相邻的 rank 尽量在同一个 die 上。RCCL 提供了RCCL_RING_ORDER环境变量export RCCL_RING_ORDER0,1,2,3,4,5,6,7具体顺序需要根据你的 GPU 拓扑来调。可以用rocm-smi --showtopo查看拓扑信息。5.4 常见问题速查表问题现象可能原因解决方法两次运行结果不同环境变量未生效设置RCCL_DEBUGINFO确认确定性模式下性能下降 40%chunk size 或协议选择不当调整RCCL_CHUNK_SIZE和RCCL_PROTO多 die GPU 上性能差ring 顺序未考虑物理拓扑设置RCCL_RING_ORDER训练过程中偶尔卡住确定性模式下的同步等待增大RCCL_TIMEOUT跨节点通信不确定网络层引入不确定性检查网络配置确保没有自适应路由FP16 归约结果不一致累加精度问题使用 FP32 累加或开启RCCL_FP16_DETERMINISTIC6. 进阶跨拓扑确定性的实现思路6.1 为什么跨拓扑确定性这么难跨拓扑确定性的核心矛盾在于归约顺序必须与拓扑无关。但 RCCL 的 ring AllReduce 的归约顺序天然依赖于 ring 的长度和顺序。4 张卡的 ring 是0-1-2-3-08 张卡的 ring 是0-1-...-7-0归约顺序完全不同。要实现跨拓扑确定性需要把归约操作分解成与拓扑无关的形式。一种思路是固定归约树不管多少张卡都按照一个固定的二叉树结构做归约。这样归约顺序只依赖于 rank 的数量不依赖于物理拓扑。但这种方法的问题是它忽略了物理拓扑的优化性能会差很多。而且 RCCL 目前没有直接支持这种模式需要在上层框架里自己实现。6.2 一种可行的折中方案如果你的场景需要跨拓扑确定性但又不想完全放弃性能可以考虑这种折中方案在 RCCL 层面开启确定性保证同一拓扑下的 run-to-run 确定性。在上层框架里做归约顺序的规范化。比如不管多少张卡都先把数据 gather 到 rank 0在 rank 0 上做串行归约然后再 scatter 回去。这样归约顺序完全由 rank 0 上的串行代码决定与拓扑无关。这种方案的性能代价很大因为 gather/scatter 的通信量是 AllReduce 的两倍。但在某些场景下比如调试这是可以接受的。6.3 实际案例在 Megatron-LM 中实现确定性训练Megatron-LM 是一个大规模训练框架对确定性有较高要求。在 ROCm 上跑 Megatron-LM 时除了设置 RCCL 的环境变量还需要注意以下几点梯度累积的顺序Megatron-LM 的梯度累积是按 micro-batch 顺序做的如果 micro-batch 的顺序不确定梯度也会不确定。需要确保数据加载器的顺序固定。流水线并行的调度流水线并行中不同 stage 的通信顺序可能影响确定性。需要固定流水线的调度策略。张量并行的归约张量并行中的 AllReduce 是确定性的关键路径。需要确保 RCCL 的确定性模式生效。具体的配置可以参考 Megatron-LM 的官方文档但要注意 ROCm 和 CUDA 的差异。比如Megatron-LM 默认使用 NCCL 的NCCL_ALGOTree在 ROCm 上需要改成RCCL_ALGORing。7. 一些踩坑之后的个人体会确定性集合通信这件事本质上是在可复现性和性能之间做取舍。没有银弹只有适合你场景的平衡点。我自己的经验是先保证正确性再优化性能。如果训练结果都不可复现性能再高也没有意义。所以我的建议是在项目初期就开启确定性模式把 baseline 跑通然后再逐步关闭非关键路径的确定性观察性能变化。另一个体会是不要迷信环境变量。RCCL 的确定性支持在不同版本、不同硬件配置下行为可能不同。一定要用实际的测试程序验证而不是假设设置了环境变量就万事大吉。最后分享一个小技巧如果你在调试确定性问题时可以把RCCL_DEBUGINFO和RCCL_DEBUG_FILE结合起来用把日志写到文件里然后用grep搜索关键信息。这样比在终端里刷屏要高效得多。export RCCL_DEBUGINFO export RCCL_DEBUG_FILE/tmp/rccl_%h_%p.log%h会替换成主机名%p会替换成进程 ID方便在多机多卡环境下区分日志。这个方向后续还可以继续深挖比如结合 ROCm 的 profiling 工具rocprof分析确定性模式下的 kernel 执行情况或者研究如何在保持确定性的同时利用 Infinity Fabric 的拓扑特性做优化。这些就留到以后有机会再聊了。