经典题解析:梯度检查点为什么能用计算换显存

图中把前向保存与反向重算分开:少留中间结果,到了需要时再计算。

训练时显存不够,除了减小批次,还可能开启梯度检查点。但它不是把模型偷偷压缩了,也不是定期把训练进度存到磁盘。这里的检查点指激活保存策略:前向少保存一些中间张量,反向求导需要它们时,再把相应前向片段重新执行。

反向为什么需要前向的信息

把网络想成依次执行的几个函数。前向输入经过每一段,产生中间表示;反向传播则根据损失对各段求导,很多算子的导数需要用到前向输入或结果。普通自动求导会保存所需张量,避免反向时重新计算。

网络越深、序列越长、批次越大,需要保留的激活就可能越多。参数和优化器状态没有增长,显存仍然可能因为这些张量变得紧张。因此先看显存由什么组成,才能判断检查点是否对症;若主要开销来自参数,它不会直接把参数存储砍掉。

留下边界,内部按需再算

假设某个片段包含三个连续算子。普通路径会保存反向需要的若干中间结果;检查点路径保留必要输入等边界信息,前向先不长期保留内部的全部激活,反向到这里时,用边界输入重新计算所需中间值,再完成求导。

这就像在一段推导旁边保存起点,而不是把每一个草稿步骤都留下。代价是重复做计算,收益是缩短部分张量的驻留时间。这里省的是特定激活,不是“整个前向不占显存”;输入、输出、仍被引用的张量和反向工作空间都可能继续存在。

少存激活,多做重算

检查点切在哪里,是一个权衡

切得很少,能释放的激活可能有限;切得很细,又会增加调度、重算和状态管理成本。不是每个片段都同样值得重算:计算昂贵但占内存少的部分,与计算便宜但激活很大的部分,适合的策略可能不同。

框架也不一定在反向时把整个片段无条件重跑到底。有的实现能够在所需中间量已备齐时提前停止,有的实现按完整片段重算。讨论额外计算量时,应说明具体接口和实现,不要把一张教学链路图推成固定的时间翻倍结论。

还要观察峰值而不是只加张量大小。如果某些大张量在时间上没有重叠,它们对峰值的影响不同。开启检查点后的实际收益需要在同一模型、输入形状和设备条件下测量,不能直接套用其他模型的百分比。

重算必须还是同一次计算

如果片段里有 dropout 等随机操作,重算使用不同随机状态,就可能产生不同掩码,从而改变梯度。框架可以保存并恢复部分随机状态,但这有额外成本,也有设备类型等适用边界,不能默认覆盖函数内部所有自定义随机源。

另一个危险来自副作用。片段如果顺手把全局计数器加一、修改缓存,或者依赖在前后两次执行之间变化的外部状态,重算就不再只是重新求一个函数值。数值形状相同,不意味着执行语义已经一致。

因此,适合检查点的片段应尽量拥有明确输入、可控随机状态和可解释的状态更新。不要把依赖外部时钟或不断变化字典的过程塞进去,再期待自动求导替你消除重复执行的影响。

先核对梯度,再讨论省多少

验证可以从很小的确定性网络开始,用完全相同的参数和输入分别运行普通路径与重算路径,对照损失和每个参数的梯度。再加入随机操作,并控制相关状态,检查差异是否符合预期,而不是只看程序有没有报错。

之后才测峰值显存和耗时,并把设备、批次、序列长度以及检查点边界写清。一个 CPU 教学函数的重算实验只能说明依赖关系,不能当成真实大模型的显存实测,更不能据此承诺节约一半显存。

和断点续训分开回答

激活检查点解决一次训练计算中的时间与空间交换;断点续训检查点解决进程中断后如何恢复模型和训练状态。前者通常不需要把这些激活持久化到磁盘,后者却需要可靠地保存可恢复状态。两个词相似,生命周期完全不同。

如果面试官继续追问“开启后梯度为什么变了”,你会先检查随机状态、函数副作用,还是直接认定这是合理的精度损失?