3、梯度累积与微批次:显存优化的核心技巧

做深度学习部署,尤其是大模型训练,最头疼的问题是什么?

显存不够。

你想想看,一张 A100 80G 的卡,跑个 7B 模型都捉襟见肘。更别提那些动辄几十亿参数的大家伙了。我刚开始做部署优化那会儿,经常因为 OOM 崩溃到怀疑人生。

后来我学乖了。与其跟显存硬刚,不如换个思路——梯度累积。

梯度累积的原理

说白了,梯度累积就是「攒着一起算」。

正常的训练流程是这样的:

  1. 前向传播算 loss
  2. 反向传播算梯度
  3. 优化器更新参数

每一步都要把中间结果留在显存里。如果你的 batch size 是 32,那就要同时处理 32 条数据,显存压力巨大。

梯度累积的做法是:

  1. 先算一个微批次(比如 4 条数据)
  2. 反向传播,但不更新参数,只把梯度累加起来
  3. 重复步骤 1-2,直到攒够 N 个微批次
  4. 用累积的梯度统一更新参数

核心公式:

有效 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 + 激活值)

其中激活值 ≈ 序列长度 × 隐藏层维度 × 层数 × 微批次大小

嗯,这个公式有点粗糙,但够用。我一般用它估算个大概,然后手动调一下。

知识体系图

梯度累积与微批次知识体系 梯度累积 原理:攒梯度 微批次大小选择 同步 vs 异步 前向+反向,不更新 有效 batch size 不变 显存占用降低 时间换空间 核心权衡 微批次越小 → 显存越低 → 训练越慢

这张图把梯度累积的核心逻辑串起来了。你看,从原理出发,到微批次选择,再到同步异步的取舍,最后落到内存与速度的权衡上。每一步都有讲究。

最后说一句:梯度累积不是银弹。如果你的模型本身就很大,微批次设为 1 都跑不动,那就要考虑模型并行或者流水线并行了。但作为入门级的显存优化手段,梯度累积绝对值得你熟练掌握。

想要更多资料,联系可可