显存不够是训练大模型绕不开的约束,梯度检查点是工程上最常用的解法之一。先把账摆清楚:训练时显存里装着三样东西——模型参数、优化器状态、前向传播产生的中间结果(激活值)。很多人以为参数最占地方,实际上批次一大,激活值经常比参数本身还大好几倍。梯度检查点做的事很直接:激活值不全存,只存少数几个检查点,反向传播需要时从最近的检查点重算一遍,用时间换显存。
激活值为什么吃显存
反向传播算梯度时,要用到前向传播每一步留下的中间结果。常规做法是这些结果全部留在显存里等反向来取。层数越多、序列越长、批次越大,激活值积累得越多。以几十层的 Transformer 为例,每一层的注意力分数、层输出都要保留,乘上批量和序列长度,总量很容易超过参数占用的好几倍——虽然参数量看起来动辄几十亿上百亿,但激活值是按"层数 × 批量 × 序列长度 × 每层宽度"涨的,一涨就是整批的量。
机制:存哪些、丢哪些、怎么重算
做法分三步。前向传播时,只在预先选定的少数层(比如每隔几层设一个)保存输出,其余层的中间结果算完就丢。反向传播开始时,从最近的检查点出发,把这一段的前向计算重新执行一遍,拿到需要的激活值,再算梯度。每个区段都如此:丢掉的补算,算完继续丢。检查点间隔是个可调的旋钮:设得越密,重算量越小、显存占用越高;设得越疏,越省显存、重算越多。框架实现里通常以每 N 层设一个检查点,N 取多少,按显存余量调。
代价是整步训练多做约一次前向传播,前向开销大约占一步训练的三分之一,所以总时间通常多两三成;收益是激活值显存能省一半以上,省出来的空间可以换更大的批次或更长的序列。具体比例随模型结构和设置浮动,以自己的训练日志为准。它和模型量化是两条不同的路:量化压的是参数精度,检查点压的是激活值存量,作用对象不同,可以叠加。
什么时候该开,什么时候别开
判断标准只有一条:显存是不是当前瓶颈。模型装不下单卡、批次开不大、想拉长序列但显存顶不住,这时开检查点,用两三成的时间换数倍的可用空间,划算。反过来,显存本来够用,开了纯属拖慢训练,没有任何额外好处。它也不提升模型能力,学出来的结果和不开时一样,纯粹是资源调度手段。
| 场景 | 做法 | 判断标准 |
|---|---|---|
| 单卡装不下模型 | 开梯度检查点,必要时叠加量化,先确保训练能跑起来 | 显存占用降到卡容量以内,且梯度数值正常 |
| 想加大批次稳定训练 | 用检查点省出的显存扩批次,而非压缩模型 | 每秒处理的样本数比不开时更高才值得开 |
| 长序列训练爆显存 | 开检查点并配合更细的检查点间隔设置 | 目标序列长度能跑通,速度下降在可接受范围 |
| 显存充足的小模型 | 不开检查点 | 没有显存压力时,开了只会白白变慢 |
和别的省显存手段怎么搭配
省显存的常用手段各有分工:混精度训练压数值精度,检查点压激活值,优化器状态分片压的是参数之外的另一块,三者作用对象不同、可以叠加。规划顺序一般是先混精度,再检查点,还不够再动批次和序列长度。讨论训练配置的资料里,这几个选项几乎总是一起出现,因为它们解决的是同一个问题:让有限的显存装下一次完整的训练步。开源权重模型的训练文档通常会把检查点列为建议配置项,照着配即可。
两个常见误区要分开说。一是把检查点和存档混淆:这里的检查点是训练过程中的中间激活值,和定期把权重保存到磁盘的 checkpoint 存档是两回事,后者占的是硬盘空间,不解决显存问题。二是以为开了检查点就能无限加大批次:省出来的是激活值那一份,参数和优化器状态的占用不随这个开关变化,批次上限仍受这两块约束。开之前先算清三块各占多少,再决定批次开多大。
梯度检查点速查
- 省的是激活值显存,代价是重算一次前向传播
- 典型量级:显存省一半以上,时间多两三成
- 只在显存是瓶颈时开,显存够用就别开
- 与量化、混精度作用对象不同,可叠加使用


