Activation Checkpointing,中文通常叫梯度检查点,也有叫激活重计算(Activation Recomputation)。
它的核心思想是:前向传播时不保存全部中间激活值,只保存少量检查点;反向传播时,从最近的检查点重新计算需要的激活值。
这样做的直接效果是:(1)显存占用大幅下降;(2)计算量增加,训练变慢。
一句话总结:用计算换显存。
训练一个神经网络,显存主要花在:
其中,中间激活值往往是最容易被忽视、但占用极大的一部分。
反向传播计算梯度时,需要用到前向传播时每一层的输出,也就是激活值。
假设网络有 $L$ 层,每层的激活值大小为 $A$,那么保存全部激活值需要:
$$ \text{Activation Memory} \approx L \times A $$
对于大模型:
所以激活值显存会随层数线性增长,成为训练显存的主要瓶颈之一。
激活值大小和 batch size 成正比。batch size 越大,激活值显存越大。
很多时候,训练 OOM 不是因为模型放不下,而是因为激活值放不下,导致 batch size 上不去。
Activation Checkpointing 就是为了解决这个问题。
普通训练流程是:
优点是反向传播快,因为激活值现成的。
缺点是显存占用高,因为要保存所有层的激活值。
使用 Activation Checkpointing 后:
这样,显存里只需要保存少量检查点,而不是全部激活值。
假设有 12 层,每 4 层设一个检查点:
这样显存从保存 12 层激活值,降到保存 4 层检查点,再加上临时重建的一小段。
普通训练保存全部 $L$ 层激活值,显存约为:
$$ O(L \times A) $$
使用 Checkpointing 后,假设每 $k$ 层设一个检查点,则只需要保存:
$$ O\left(\frac{L}{k} \times A\right) $$
再加上反向时临时重建的 $O(k \times A)$。
当 $k$ 较大时,显存占用显著下降。
如果整个网络只设一个检查点,也就是只保存输入:
显存最省,但计算代价最大。
如果网络有 $L$ 层,每层激活值相同,理论最优的检查点间隔约为:
$$ \sqrt{L} $$
此时显存占用和计算代价之间达到较好的平衡:
这就是经典的平方根法则。
反向传播时,需要重新计算被丢弃的激活值,相当于多做一次前向。
普通训练一次迭代的计算量约为:
$$ \text{前向} + \text{反向} \approx 1 + 2 = 3 $$
使用 Checkpointing 后,计算量约为:
$$ \text{前向} + \text{重算前向} + \text{反向} \approx 1 + 1 + 2 = 4 $$
所以训练时间通常会增加约 20% 到 30%,具体取决于检查点粒度和实现。
因为多了重算,GPU 计算时间增加,吞吐下降。
如果训练本身是计算受限,而不是显存受限,那么开启 Checkpointing 会得不偿失。
需要:
有些层计算便宜但激活值大,适合设检查点;有些层计算昂贵但激活值小,设检查点反而亏。
所以实际中往往只对部分层开启,比如 Transformer 的某些块。
Activation Checkpointing 通常不是单独使用,而是和以下技术组合:
其中,Checkpointing 主要解决激活值显存,而 ZeRO 主要解决模型状态显存,两者互补。
torch.utils.checkpoint、DeepSpeed、Megatron 都支持。torch.utils.checkpoint 是怎么实现的?这是一道很好的大模型训练显存优化面试题,适合考察候选人对激活值显存和计算换显存思想的理解。它既能考察基础原理,也能延伸到工程调优。如果候选人能把 Checkpointing 的原理、显存节省来源、计算代价、平方根法则以及适用场景都讲清楚,基本可以判断其具备大模型训练显存优化的扎实基础。
评论专区
评论加载中...登录后即可发表评论