参考了 (Hashimoto & Liang, 2026; Tazi et al., 2025)。
Naïve Data Parallelism
最拿衣服的 DP 大概就是每张卡存个模型,每张卡然后算出 gradient 然后 All-Reduce。但 All-Reduce 的时候 GPU 也没啥别的事情做。一个简单的优化方法是,反向传播的时候,我们每经过一个 block 就 async 地触发那一层地 All-Reduce。使用 torch.Tensor.register_post_accumulate_grad_hook 就可以做。
然后如果每个小 block 都触发一次 All-Reduce,就比较零散。所以优化方案是几个 block bucket 起来做一次 All-Reduce。
然后对于 gradient accumulation 的时候,optimizer.step() 的前一步才需要执行 All-Reduce。
Zero Redundancy Optimizer (ZeRO)
\(\Psi\) 为参数量,\(N_d\) 为数据并行度。参数与梯度按 16-bit 计,优化器状态为每参数 \(k\) bytes;色块高度按混合精度 Adam 的 \(k = 12\) 绘制(FP32 主权重及一、二阶矩)。仅计模型状态,不含激活与临时通信缓冲区。
我们计算一下通信量。我们就不管 \(\frac{N_d-1}{N_d}\) 的系数了,假设一次 Reduce-Scatter 或者一次 All-Gather 是 \(2\Psi\),一次 All-Reduce 是 \(4\Psi\)。
对于 Naïve DP,最后需要 All-Reduce gradient 也就是 \(4\Psi\)。
对于 ZeRO-1,切分了 optimizer states。首先单张卡走 forward 和 backward。这里有一个小技巧,就是对于 AdamW,如果知道一部分梯度,就可以算出那部分的一阶和二阶 moment 从而来更新模型参数。因此我们可以直接一个 Reduce-Scatter 计算平均并分发梯度,每张卡只负责更新自己那一小部分模型。最终把更新好的模型 All-Gather 起来。于是通信量是 \(2\Psi+2\Psi=4\Psi\),其实相比于 Naïve DP 没有特别多损失。对于 Muon 就比较复杂,因为他没有 AdamW 的性质,这边暂时不讨论。
对于 ZeRO-2,切分了 optimizer states 和 gradient。事实上和 ZeRO-1 是一样的。因为我们不难观察到 ZeRO-1 中,backward reduce-scatter 之后,每张卡只需要保存部分梯度,别的梯度可以立刻释放掉。所以通信仍然是一次 Reduce-Scatter 一次 All-Gather,也就是 \(4\Psi\)。
对于 ZeRO-3,把模型参数也给切分了。这时候每次 forward 我们需要按层 All-Gather 聚合模型参数,用完就释放完整参数,所以 backward 前还需要再次 All-Gather 本层参数。梯度做 Reduce-Scatter 后,每张卡更新并保留自己的参数分片,等下次 forward 时再聚合。于是通信量是两次参数 All-Gather 加一次梯度 Reduce-Scatter,也就是 \(2\Psi+2\Psi+2\Psi=6\Psi\)。
ZeRO-3 的前向、反向框分别对每层重复,仅临时聚合当前层参数。通信量沿用正文的 16-bit 口径,忽略 \(\frac{N_d-1}{N_d}\);逐层 / bucket 的通信量按整个模型求和,不表示只有一次 API 调用。图中不展开预取与通信计算重叠。