4. 混合同步(Hybrid Sync):梯度累积、局部同步与全局同步、跨节点同步策略
说到分布式训练,很多人第一反应就是All-Reduce。但说实话,真正在生产环境里跑过千卡集群的人都知道——纯同步或纯异步,都有各自的坑。
我最早接触混合同步,是在一次跨机房的训练任务中。当时两个机房之间的网络延迟高达几十毫秒,纯同步根本跑不动。后来我们换了一种思路:机内全同步,机间半同步。嗯,这就是混合同步的雏形。
4.1 为什么需要混合同步?
先看几个现实问题:
- 网络拓扑不均:同一台机器内的GPU通过NVLink通信,带宽可达600GB/s;但跨机器的网络,哪怕用InfiniBand,也就100-200Gbps。差了30倍。
- 规模扩展瓶颈:当GPU数量超过256张,纯同步的「木桶效应」越来越明显——最慢的那张卡拖死所有人。
- 资源利用率:异步虽然快,但模型收敛质量不稳定。我见过有人用异步跑了三天,loss死活降不下去,最后发现是梯度延迟太大。
混合同步的核心思想就一句话:在快的地方同步,在慢的地方妥协。
混合同步的本质:将通信拓扑分层,每层采用不同的同步策略,在收敛速度与通信开销之间取一个平衡点。
4.2 梯度累积:先攒后发
梯度累积不是新鲜事,但它是混合同步的基础组件。说白了就是:不急着同步,先攒几个step的梯度,攒够了再一次性通信。
为什么要这么做?
- 减少通信频率:原本每个step都要All-Reduce,现在每N个step才做一次,通信量直接降到1/N。
- 等效增大Batch Size:梯度累积N步,相当于把batch size扩大了N倍。这对BN层的统计量有影响,需要注意。
- 掩盖通信延迟:在通信的同时,可以继续计算下一轮的梯度。
代码实现其实很简单:
# 梯度累积伪代码
accumulation_steps = 4
optimizer.zero_grad()
for i, (inputs, labels) in enumerate(dataloader):
outputs = model(inputs)
loss = criterion(outputs, labels)
loss = loss / accumulation_steps # 注意这里要除
loss.backward()
if (i + 1) % accumulation_steps == 0:
# 真正做梯度同步和参数更新
all_reduce_gradients(model)
optimizer.step()
optimizer.zero_grad()
我曾经踩过的坑:梯度累积时,loss一定要除以accumulation_steps。否则累积N步后的梯度相当于N倍,学习率得跟着调。我第一次写的时候忘了除,模型直接炸了。
4.3 局部同步与全局同步
这是混合同步的核心分层策略。我们通常把集群分成两层:
- 局部同步(Local Sync):同一节点内的GPU之间,做全量All-Reduce。因为NVLink很快,延迟低,同步代价小。
- 全局同步(Global Sync):不同节点之间,做稀疏同步或延迟同步。因为网络慢,不能每步都等。
具体怎么配合?我画了一张图来说明:
你看,节点内部用实线(NVLink)做全量同步,节点之间用虚线(以太网)做稀疏同步。这样既保证了单机内的梯度一致性,又避免了跨机通信成为瓶颈。
4.4 跨节点同步策略
跨节点同步是混合同步中最灵活、也最需要调参的部分。我总结了几种常见策略:
| 策略名称 | 核心思想 | 通信频率 | 收敛质量 | 我推荐的使用场景 |
|---|---|---|---|---|
| 周期同步 | 每K个step做一次全局All-Reduce | 低 | 高 | 网络稳定、模型较大时 |
| 梯度压缩同步 | 只同步Top-k%的梯度,其余丢弃 | 中 | 中 | 带宽受限、梯度稀疏时 |
| 延迟同步 | 允许节点落后一定步数,超时则跳过 | 高 | 中低 | 异构集群、节点性能差异大时 |
| 异步局部+同步全局 | 机内异步更新,机间周期同步 | 低 | 中 | 机内GPU数量多、机间网络慢时 |
我个人习惯:刚开始调混合同步时,先试周期同步,K从4开始。如果loss曲线正常,再逐步增大K。如果发现收敛变慢,就减小K。梯度压缩同步虽然省带宽,但需要额外调压缩比,有点麻烦。
4.5 实战中的避坑指南
混合同步看起来美好,但落地时坑不少。我挑几个常见的说说:
- Batch Normalization 的统计量:梯度累积时,BN的running mean和running var是按每个micro-batch更新的。如果你累积了4步,BN实际上看到了4倍的数据,但统计量更新频率没变。这会导致推理时BN统计量不准。解决办法是:在累积的最后一步,用累积后的梯度更新BN统计量。
- 学习率调整:梯度累积等效于增大batch size。一般来说,batch size翻倍,学习率可以适当增大。但不要线性放大——我见过有人把batch size从32提到128,学习率也乘以4,结果直接发散。建议用平方根缩放:lr_new = lr_old * sqrt(batch_new / batch_old)。
- 通信与计算重叠:这是性能优化的关键。在梯度累积的最后一个step,启动All-Reduce的同时,可以开始下一轮的第一个step的前向计算。只要计算时间能覆盖通信时间,通信开销就「隐藏」了。
我曾经犯过的错:有一次做64卡训练,用了梯度累积+周期同步。我天真地以为K越大越好,设了K=16。结果跑了半天,loss纹丝不动。后来发现,梯度累积16步后,梯度已经「过时」了——参数更新了16次,但梯度还是16步前的。这相当于用旧梯度更新新参数,收敛当然慢。后来我把K改回4,问题就解决了。
4.6 小结
混合同步不是银弹,但它给了我们一个灵活的框架:在快的地方做全同步保证精度,在慢的地方做稀疏同步保证效率。梯度累积是基础,局部同步是核心,全局同步策略是调优空间。
如果你正在搭建一个多机多卡的训练系统,我建议先从纯同步开始,跑通后再逐步引入混合同步。别一上来就搞复杂策略——先让模型能收敛,再谈优化。