3、梯度累积与微批次:显存优化的核心技巧
做深度学习部署,尤其是大模型训练,最头疼的问题是什么?
显存不够。
你想想看,一张 A100 80G 的卡,跑个 7B 模型都捉襟见肘。更别提那些动辄几十亿参数的大家伙了。我刚开始做部署优化那会儿,经常因为 OOM 崩溃到怀疑人生。
后来我学乖了。与其跟显存硬刚,不如换个思路——梯度累积。
梯度累积的原理
说白了,梯度累积就是「攒着一起算」。
正常的训练流程是这样的:
- 前向传播算 loss
- 反向传播算梯度
- 优化器更新参数
每一步都要把中间结果留在显存里。如果你的 batch size 是 32,那就要同时处理 32 条数据,显存压力巨大。
梯度累积的做法是:
- 先算一个微批次(比如 4 条数据)
- 反向传播,但不更新参数,只把梯度累加起来
- 重复步骤 1-2,直到攒够 N 个微批次
- 用累积的梯度统一更新参数
核心公式:
有效 batch size = 微批次大小 × 累积步数
显存占用 ≈ 微批次大小(而不是有效 batch size)
我在项目中遇到过这样一个场景:客户要求用 7B 模型做微调,但只有 4 张 24G 的卡。直接跑 batch size=32 肯定炸。我把微批次设为 2,累积 16 步,等效 batch size 还是 32,但显存占用只有原来的 1/16。
嗯,这就是梯度累积的魔力。
微批次大小怎么选?
这个问题没有标准答案。但我可以给你一些经验值。
| 模型规模 | 单卡显存 | 推荐微批次 | 累积步数 |
|---|---|---|---|
| 7B | 24G | 1-2 | 16-32 |
| 13B | 40G | 2-4 | 8-16 |
| 70B | 80G | 1 | 32-64 |
我个人习惯是:先测最大能跑多少。把微批次从 1 开始往上加,直到 OOM 为止。然后取这个值的 80% 作为安全值。
为什么不是 100%?
因为训练过程中显存使用会有波动。我曾经为了省显存把微批次压到极限,结果跑着跑着就炸了。后来我学乖了,留 20% 的余量。
小技巧:如果你用 PyTorch,可以用 torch.cuda.max_memory_allocated() 实时监控显存峰值。我一般会在训练脚本里加个 hook,每 100 步打印一次显存使用情况。
同步 vs 异步梯度更新
这里有个容易踩的坑。
同步梯度更新:每个微批次算完梯度后,等所有 GPU 的梯度都同步完,再累加。
- 优点:梯度准确,收敛稳定
- 缺点:慢,因为要等最慢的那张卡
异步梯度更新:每个 GPU 算完自己的梯度就直接累加,不等别人。
- 优点:快,没有等待时间
- 缺点:梯度有延迟,收敛可能不稳定
我刚开始做分布式训练时,觉得异步肯定更好,毕竟快嘛。结果模型训了三天,loss 死活降不下去。后来排查发现,异步梯度更新导致参数更新方向不一致,模型在震荡。
避坑指南:我曾经在 8 卡环境下用异步梯度更新,结果模型收敛速度比同步慢了 3 倍。后来我改成同步,虽然每步慢了 20%,但总训练时间反而缩短了。
我的建议是:除非你非常清楚自己在做什么,否则用同步。
内存与速度的权衡
这是个老生常谈的问题,但我想换个角度说。
你想想看,梯度累积的本质是什么?
是用时间换空间。
微批次越小,显存占用越少,但训练时间越长。因为你要多跑好几轮前向+反向。
我一般会画个曲线图:
- 横轴:微批次大小
- 纵轴:训练时间 / 显存占用
两条曲线会有一个交叉点。那个点就是最优解。
我的经验公式:
最优微批次 ≈ 显存上限 / (模型参数 × 2 + 激活值)
其中激活值 ≈ 序列长度 × 隐藏层维度 × 层数 × 微批次大小
嗯,这个公式有点粗糙,但够用。我一般用它估算个大概,然后手动调一下。
知识体系图
这张图把梯度累积的核心逻辑串起来了。你看,从原理出发,到微批次选择,再到同步异步的取舍,最后落到内存与速度的权衡上。每一步都有讲究。
最后说一句:梯度累积不是银弹。如果你的模型本身就很大,微批次设为 1 都跑不动,那就要考虑模型并行或者流水线并行了。但作为入门级的显存优化手段,梯度累积绝对值得你熟练掌握。
想要更多资料,联系可可