详细解释
Gradient Accumulation(梯度累积,简称 Grad Accum 或 GA)一句话:你想用 Batch Size = 128 训一个 7B 模型(保证 Batch Norm / Loss 稳定性 / 大 Batch 泛化性这些好处),但你的 4090 24G 一次最多塞 BS=16;梯度累积就是把 128 拆成 8 次 Micro-Batch,每次拿 16 个样本算一次 Forward+Backward 得到梯度后不更新权重,把梯度累加起来;等做完 8 次 Micro-Batch 之后,累积出来的梯度就等效于一次性算 BS=128 的梯度了,这时候才调用 optimizer.step + scheduler.step + zero_grad 更新一次权重。这样就用「每 8 个 step 更新一次权重」的时间换了「模拟 8 倍大 Batch」的效果。它是 2022 年之后大模型微调的「平民神器」——没有它,普通人根本没法用消费级显卡微调大模型。
梯度累积的数学等效性:只要你在累积 N 个 Micro-Batch 时,对每个 Micro-Batch 的 Loss 除以 N(做归一化),然后不做梯度裁剪 / optimizer.step,只累积梯度,那么 N 次累积之后的梯度,和真实把 N 个 Micro-Batch 拼成一个 Mega-Batch 一次性算出来的梯度数学上完全相等(前提是你的模型没有 BatchNorm 这种跨样本统计归一化的层——LLM 都是 LayerNorm,没问题),最终收敛曲线和效果基本完全一样,不会有任何精度损失。唯一的区别是训练时间变长(原来 1000 step 更新 1000 次,现在 1000×8 step 只更新 1000 次,耗时接近 ×8,因为要算更多次 Backward),但 GPU 峰值显存占用直接降到 1/N。
唯元智创 的 SFT / DPO 云训练平台,在你选择小 GPU(4090 / A10)时会自动帮你算最优的 Batch Size 和 Gradient Accumulation 步数组合:目标是让 Micro-Batch 尽量大(提高 GPU 利用率、计算效率高)、累积步数刚好凑够你想要的等效 Batch Size(通常 128 / 256 对 SFT 最优),而且会自动处理 Loss 缩放、梯度裁剪时机、Learning Rate 调度器更新时机这些容易踩坑的细节,用户不用自己调。
用梯度累积的 4 个核心等效关系(只要这几条满足就和真大 Batch 没差别)
| 维度 | 真实一次性大 Batch BS=B | Gradient Accumulation N 步,Micro-Batch = b,B = N × b |
|---|---|---|
| 梯度数值 | 对 B 条样本平均 Loss 后 Backward,得到 Grad_true | 每个 Micro-Batch 的 Loss 先除以 N,Backward 得到 grad_i;累积 Sum(grad_i) 之后再更新 → Grad_accum |
| 梯度等效性 | Grad_true | Grad_accum 严格等于 Grad_true(数学上,无 LayerNorm LLM 场景下) |
| 权重更新频率 | 每 1 step 更新 1 次(1 step = 1 次 Forward/Backward + 1 step 更新) | 每 N 个 Micro-Step 更新 1 次(前面 N-1 步只累积梯度不更新) |
| Learning Rate 调度更新时机 | 每次权重更新之后,scheduler.step 一次 | 每 N 个 Micro-Step(做完一次真正的权重更新)之后 scheduler.step 一次——绝对不能每个 Micro-Step 调 scheduler.step(会让 LR 下降速度变成原来的 1/N,训练直接崩) |
| 梯度裁剪时机 | 每次 Backward 之后、optimizer.step 之前 clip | 累积完 N 步之后、optimizer.step 之前做一次全局 clip;不能每个 Micro-Step 单独 clip(先 clip 再相加和加完再 clip 结果完全不同) |
| BatchNorm/LayerNorm 统计量 | BatchNorm 统计 B 条样本均值方差;LayerNorm 不跨样本 | LLM 都是 LayerNorm,对 Micro-Batch 大小不敏感,所以完全没问题;如果你训的是 CV 小模型带 BatchNorm,GA 会损失精度(因为 BatchNorm 在 Micro-Batch 上算统计量不是在大 Batch 上算),这个时候 GA 不是严格等效 |
| 训练总步数配置 | Epoch × 样本数 / B 个 update step | Epoch × 样本数 / B 个 update step(和左边一样,update 步数不变);总 Micro-Step 数 = N × Update_Steps(比原来多 N 倍) |
落地梯度累积的 5 个坑(90% 的人第一次用都会踩至少 3 个)
- 坑 1:忘了给每个 Micro-Batch 的 Loss 除以累积步数 N——这是 GA 第一大坑,踩了之后你的等效学习率相当于被放大了 N 倍,训练一开始 Loss 直接飞上天然后发散。直观理解:真实大 Batch 的 Loss 是 B 条样本的平均(÷ B);你每个 Micro-Batch 的 Loss 是 b 条的平均(÷ b),直接加 N 个的话,总和比真实大 Batch Gradient 大了 N 倍;正确做法是每个 Micro-Batch Loss 先 ÷ N,这样 N 个 ÷N 之后的 Loss 梯度加起来,刚好等于真实 ÷ B 的大 Batch Loss 梯度。
- 坑 2:每个 Micro-Step 都调了 scheduler.step 和 optimizer.step——只有「第 N 步、第 2N 步、第 3N 步」这些刚好累积完一轮的时候才调用 optimizer.step 和 scheduler.step,其余 Micro-Step 只算 loss.backward() 不做任何更新;90% 的新手上来就把 optimizer.step() 写在每个 for batch 循环里,那和没用 GA 完全一样,白浪费 N-1 倍时间。
- 坑 3:在每个 Micro-Step 做完 backward 之后就梯度裁剪了——应该等 N 步累积完梯度之后、optimizer.step 之前做一次全局 clip;每个 Micro-Step 单独 clip 之后再相加,等效于「把每个分量的梯度先上限截断,再求和」,和「先求完整大 Batch 梯度再整体 clip」的结果完全不一样,会严重影响收敛。
- 坑 4:Multi-GPU DDP 场景下的梯度同步时机错了——多卡 DDP 默认每个 backward 之后会自动做 all-reduce 跨卡同步梯度;你用 GA 时前 N-1 步根本不需要同步(跨卡同步的目的就是为了更新权重做准备,不更新权重同步纯粹浪费带宽)。正确做法是用 PyTorch
model.no_sync()上下文管理器把前 N-1 个 Micro-Batch 的 backward 包起来,只有第 N 步才做正常带同步的 backward;这样能省 N-1 次 all-reduce 的网络开销,多卡 GA 的速度能提升 20-40%。 - 坑 5:把 Micro-Batch 调得太小(比如 1 或 2)——很多人觉得「反正 GA 能累积,Micro-Batch 越小越好,一张 4090 能塞下 7B 模型就行」,但 Micro-Batch 太小(< 4)时,GPU 的 Tensor Core 利用率会从 70% 掉到 30%,计算效率暴跌;同样一个 Micro-Batch 从 1 调到 16,GPU 利用率涨 2 倍,完成 1 个 update step 的时间反而差不多。经验设置:让 Micro-Batch 把显存吃到 90%(留 10% 给 KV Cache/碎片),然后用 GA 步数凑够等效 Batch Size,这是最高性价比的组合。