【Bug已解决】[Question] To achieve consistent computation results under disparate device quantity configu

【Bug已解决】[Question] To achieve consistent computation results under disparate device quantity configu
【Bug已解决】[Question] To achieve consistent computation results under disparate device quantity configurations. 解决方案一、现象长什么样同一份训练代码、同一个随机种子、同一份数据换不同的卡数跑结果却不一样2 卡训练的 loss 曲线和 4 卡、8 卡训练的对不上最终精度差一截或 early step 的 loss 就不同。单卡和 8 卡跑同一配置global_batch_size没特意对齐时差得最明显。即便把global_batch_size调成一样前向/反向的数值末几位仍可能不同影响可复现性科研实验要能复现。特征不是崩溃是「换卡数就不一致」——分布式训练的可复现性痛点。卡数越多每 rank 每步处理的样本越少若per_device_bs固定batch 切分边界变化 → 梯度统计不同。纯数据并行DP下最典型混合并行TP/PP还叠加通信顺序差异。本质不同设备数量下「每个 rank 每步看到哪些样本、梯度如何在多少 rank 间聚合、运算以什么顺序发生」都不一样于是数学上等价的计算在数据划分与浮点累加顺序上产生差异结果不可复现。二、背景要让「不同卡数结果一致」得先搞清楚哪些因素随卡数变每 rank 的样本组成global_batch per_device_bs × world_size。卡数变了若per_device_bs不变global_batch就变 → 一次 update 看的样本不同 → 梯度天然不同这其实是「该不同」的因为总 batch 变了。要一致必须固定 global batch size让卡数变化只改变「多少卡分担」不改变「每步总样本数」。梯度聚合的 rank 数DP 下梯度在 N 个 rank 间 all-reduce 平均。N 不同平均的「参与者集合」不同浮点加法结合律不严格成立FP32 累加顺序影响末位结果末位不同。BatchNorm / 统计量的聚合若用了跨卡同步 BNSyncBN参与统计的卡数变了running mean/var 的估计来源变了。随机性与种子每 rank 的种子若按 rank 派生卡数变化会改变「哪些 rank 用哪些随机子序列」dropout/masking 结果变。数据划分边界不同 world_size 下DistributedSampler把样本分给各 rank 的方式不同 → 同一个 global batch 内部样本顺序/分组不同。一句话要一致必须让「global batch 组成 梯度聚合语义 随机种子」在不同卡数下保持等价且接受浮点末位差异或用确定性累加。三、根因可复现性缺口分析把这个当 bug 分析根因是不同设备数量改变了「global batch 组成 / 梯度聚合参与者 / 随机种子派生 / 数据划分边界」导致数学等价但数值不一致三层第一层主因global_batch_size 没随卡数固定。用户固定per_device_bs卡数变 →global_batch变 → 每步看不同样本 → 梯度本质不同这是配置问题不是 bug但导致「不一致」。第二层浮点累加顺序随 rank 数变化。all-reduce 平均在 N 个 rank 间做N 不同累加顺序不同FP32 下末位有差SyncBN 的统计量也随参与卡数变。这是「数学相等但数值不等」的主因。第三层种子按 rank 派生卡数变则子序列变。manual_seed(base rank)在卡数变化时各 rank 拿到的随机子序列整体平移dropout/mask 结果不可复现。一句话global batch 未固定 FP 累加顺序随 rank 数变 种子派生随卡数变三者让不同卡数结果不可复现。四、最小可运行复现下面用纯 Python 模拟「相同 global batch 但不同 world_size 下梯度平均的浮点累加顺序导致末位差异」不需要 GPUdef all_reduce_avg(grads_per_rank): 模拟 N 个 rank 的梯度做平均顺序依赖参与者数量。 total 0.0 for g in grads_per_rank: total g # 累加顺序依赖 rank 数量/顺序 return total / len(grads_per_rank) def main(): # 假设每 rank 的局部梯度近似值 g2 [0.1, 0.2] # 2 卡 g4 [0.1, 0.2, 0.05, 0.25] # 4 卡同 global batch但划分不同 avg2 all_reduce_avg(g2) avg4 all_reduce_avg(g4) print(f2 卡平均梯度: {avg2:.10f}) print(f4 卡平均梯度: {avg4:.10f}) print(f差值(末位): {abs(avg2 - avg4):.2e}) if __name__ __main__: main()跑出来两个平均值末位有差——演示了「聚合参与者/顺序不同导致数值不等」即便 global batch 一样。五、解决方案第一层最小直接修复最省事的救火固定 global batch size 线性缩放学习率 用梯度累积补偿卡数让换卡数时「每步总样本数」不变from accelerate import Accelerator accelerator Accelerator() BASE_GLOBAL_BS 64 base_lr 1e-4 world accelerator.num_processes per_device_bs BASE_GLOBAL_BS // world # 卡多了 per_device_bs 变小用梯度累积补回 global batch grad_accum 1 if per_device_bs 1: grad_accum world per_device_bs max(1, BASE_GLOBAL_BS // (world * grad_accum)) # 线性 LR 缩放保持 global batch 对应的有效 LR effective_lr base_lr * (BASE_GLOBAL_BS / (per_device_bs * world)) dl accelerator.prepare(DataLoader(ds, batch_sizeper_device_bs))这样无论 2/4/8 卡global_batch都是 64每步看的样本相同梯度语义一致末位浮点差异仍存在但数学等价。六、解决方案第二层结构性改进第一层是「固定 global batch LR 缩放」第二层是「再加确定性保障固定随机种子派生、确定性累加、一致数据划分」从设计上逼近跨卡数复现import random, numpy as np, torch def make_deterministic(base_seed: int, world: int, rank: int): 种子派生不随 world 平移用 (base, rank) 固定映射且跨卡数可复现。 # 关键用哈希而非简单 rank避免 world 变化导致子序列整体平移 seed (base_seed * 1000003 rank * 7919) 0xFFFFFFFF random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) torch.use_deterministic_algorithms(True, warn_onlyTrue) return seed dataclass class ReproConfig: base_global_bs: int 64 base_lr: int 1e-4 def plan(self, world: int): per_device self.base_global_bs // world accum 1 if per_device 1: accum (world self.base_global_bs - 1) // self.base_global_bs per_device max(1, self.base_global_bs // (world * accum)) eff_lr self.base_lr * self.base_global_bs / (per_device * world) return per_device, accum, eff_lr # 用法 cfg ReproConfig() per_device, accum, lr cfg.plan(worldaccelerator.num_processes) make_deterministic(base_seed42, worldaccelerator.num_processes, rankaccelerator.process_index)额外的「一致数据划分」技巧用一个全局固定的 batch 序列先按global_batch切好有序 batch 列表再按 rank 分配保证不同 world_size 下同一个 global batch 包含的样本集合相同只是内部分给各 rank 的方式变从而梯度聚合的数学对象一致。七、解决方案第三层断言 / CI 守护把「global batch 固定」「LR 线性缩放」「种子派生稳定」「聚合等价」固化成测试import pytest def test_global_batch_fixed_across_world(): cfg ReproConfig(base_global_bs64) for world in (1, 2, 4, 8): pd, accum, _ cfg.plan(world) assert pd * world * accum 64 # global batch 恒为 64 def test_lr_linear_scaling(): cfg ReproConfig(base_global_bs64, base_lr1e-4) _, _, lr2 cfg.plan(2) _, _, lr8 cfg.plan(8) # 有效 LR 都应回到 base_lrglobal batch 相同 assert abs(lr2 - 1e-4) 1e-9 assert abs(lr8 - 1e-4) 1e-9 def test_seed_stable_across_world(): # rank 0 在不同 world 下应派生出相同种子可复现 s1 make_deterministic(42, world2, rank0) s2 make_deterministic(42, world8, rank0) assert s1 s2 def test_grad_accum_compensates(): cfg ReproConfig(base_global_bs64) pd, accum, _ cfg.plan(world16) # 每卡装不下用累积 assert pd * 16 * accum 64 def test_aggregation_equivalent(): # 数学等价相同 global batch不同划分均值应一致忽略 FP 末位 g2 [0.1, 0.2] g4 [0.1, 0.2, 0.0, 0.1] # 同 global batch 的另一种划分 avg2 sum(g2) / len(g2) avg4 sum(g4) / len(g4) assert abs(avg2 - avg4) 1e-9再加一个端到端回归2 卡与 8 卡跑同配置loss 曲线在容差内一致def test_consistent_across_device_counts(): for world in (2, 8): acc make_accelerator(world) cfg ReproConfig(base_global_bs64) pd, accum, lr cfg.plan(world) make_deterministic(42, world, acc.process_index) loss run_steps(acc, per_devicepd, accumaccum, lrlr, steps5) record(world, loss) assert curves_close(record[2], record[8], tol1e-3) # 数学一致八、排查清单看换卡数 loss 曲线明显分叉 → 先确认 global_batch_size 是否随卡数变了最常见。固定global_batch_size用梯度累积 线性 LR 缩放补偿卡数。设确定性固定种子派生不随 world 平移、torch.use_deterministic_algorithms(True)。若用 SyncBN注意统计量随参与卡数变是预期可考虑固定 BN 或关 SyncBN 做复现。接受浮点末位差异跨卡数完全一致通常只到「数学等价 末位容差」非要逐位一致需确定性累加。升级 accelerate 到合了复现工具Reporter/seed 工具的版本并跑上面的test_global_batch_fixed_across_world。用全局固定 batch 序列保证不同 world 下样本组成一致。九、小结不同设备数量下结果不一致不是框架 bug而是global batch 组成、梯度聚合参与者/顺序、随机种子派生都随卡数变了数学等价但数值不可复现。最小修复是固定 global batch size 线性 LR 缩放 梯度累积补偿结构性修复是加确定性种子派生、确定性累加、全局固定 batch 划分最后用 pytest 把「global batch 固定」「LR 缩放」「种子稳定」「聚合等价」锁死。抓住「换卡数只应改变并行度、不应改变每步看到的样本集合与有效学习率」这条所有分布式可复现性痛点都能照此对齐。

最新新闻

日新闻

周新闻

月新闻