4. 梯度累积:原理与实现,Batch Size与梯度累积的关系
说到梯度累积,我得先坦白一件事。
我刚开始做大模型训练那会儿,经常遇到一个让人抓狂的问题——明明代码写得没问题,显卡也够新,可一跑起来就报 OOM。后来我发现,很多时候不是显存不够,而是我们太贪心了,总想把 Batch Size 设得很大。
梯度累积,说白了就是「拆东墙补西墙」的聪明做法。它让你用小显存,跑出大 Batch Size 的效果。
4.1 为什么需要梯度累积?
先想一个问题:为什么大 Batch Size 训练效果好?
因为梯度更稳定,收敛更平滑。但大 Batch Size 意味着一次要加载更多数据,显存扛不住。
那怎么办?
梯度累积的思路很简单:
- 不一次性算完所有样本的梯度
- 而是分几次算,每次算一小批
- 把梯度累加起来,最后统一更新参数
举个例子。你想用 Batch Size = 64 训练,但显存只够跑 Batch Size = 16。那就分 4 步走:
- 前 16 个样本,算梯度,存起来
- 再 16 个样本,算梯度,累加上去
- 再 16 个,再累加
- 最后 16 个,累加完,统一更新参数
嗯,这就是梯度累积的核心逻辑。
核心公式:
有效 Batch Size = 单次 Batch Size × 累积步数
比如单次跑 16,累积 4 步,效果等价于 Batch Size = 64
4.2 Batch Size 与梯度累积的关系
这里有个容易踩的坑。我刚开始用梯度累积时,以为只要累积步数够多,效果就完全等价于大 Batch Size。其实不是。
| 单次 Batch Size | 累积步数 | 有效 Batch Size | 显存占用 | 训练效果 |
|---|---|---|---|---|
| 16 | 1 | 16 | 低 | 梯度噪声大 |
| 16 | 4 | 64 | 低 | 接近 64 的效果 |
| 64 | 1 | 64 | 高 | 标准效果 |
| 16 | 8 | 128 | 低 | 可能梯度爆炸 |
为什么累积太多步会出问题?
因为梯度累积本质上是把多个小 Batch 的梯度「平均化」。如果累积步数太多,梯度更新频率变低,模型收敛会变慢。我遇到过有人把累积步数设到 32,结果训练了三天 loss 纹丝不动。
避坑指南:
我曾经犯过一个错误——累积步数设得太大,导致梯度爆炸。后来发现,累积步数最好控制在 2-8 之间。超过 8 步,建议检查一下学习率是否需要调低。
4.3 梯度累积的实现原理
实现起来其实不复杂。核心就三步:
- 前向传播:正常算 loss
- 反向传播:算梯度,但不更新参数
- 梯度累积:把梯度累加到缓存里
- 参数更新:累积够步数后,统一更新
这里有个关键点:每次反向传播后,梯度是累加的,不是覆盖的。PyTorch 默认会累积梯度,所以很多人不小心就踩了坑——忘了清零梯度。
小技巧:
我个人习惯在每次累积开始时,手动调用 optimizer.zero_grad()。虽然 PyTorch 会自动累积,但显式清零更安全,不容易出 bug。
4.4 实战代码示例
直接上代码。这是一个完整的梯度累积训练循环:
import torch
import torch.nn as nn
import torch.optim as optim
# 假设模型和数据
model = nn.Linear(10, 2)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
# 梯度累积参数
accumulation_steps = 4 # 累积 4 步
current_step = 0
for epoch in range(10):
for batch_idx, (data, target) in enumerate(dataloader):
# 1. 前向传播
output = model(data)
loss = criterion(output, target)
# 2. 反向传播(梯度会累积)
loss.backward()
current_step += 1
# 3. 累积够步数后,更新参数
if current_step % accumulation_steps == 0:
optimizer.step() # 更新参数
optimizer.zero_grad() # 清零梯度
# 可选:打印 loss
print(f'Epoch {epoch}, Step {current_step}, Loss: {loss.item():.4f}')
这段代码看着简单,但有几个细节要注意:
- loss 要除以累积步数:如果不除,累积后的 loss 会变大,梯度也会变大。我一般这样写:
loss = loss / accumulation_steps - 清零时机要对:一定要在 optimizer.step() 之后清零,否则梯度会一直累积下去
- 学习率要调整:累积步数越大,有效 Batch Size 越大,学习率可以适当调大一点
进阶技巧:
如果你用混合精度训练(AMP),梯度累积要稍微改一下。因为 AMP 会缩放梯度,累积时要注意缩放因子的处理。我一般这样写:
with torch.cuda.amp.autocast():
output = model(data)
loss = criterion(output, target)
loss = loss / accumulation_steps
scaler.scale(loss).backward()
if (batch_idx + 1) % accumulation_steps == 0:
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
4.5 知识体系图
下面这张图总结了梯度累积的核心逻辑:
4.6 总结一下
梯度累积不是什么高深的技术,但它确实能解决很多实际问题。我个人的经验是:
- 累积步数 2-4 步最稳妥,效果和显存占用平衡得最好
- 记得把 loss 除以累积步数,不然梯度会偏大
- 混合精度训练时,注意 scaler 的用法
- 如果累积步数超过 8,建议调低学习率
说白了,梯度累积就是「用时间换空间」。虽然训练时间会变长一点,但至少不会 OOM 了。你想想看,与其让训练中断,不如多等一会儿,对吧?
最后一个小建议:
如果你刚开始用梯度累积,建议先从累积 2 步开始试。跑通了再慢慢加。我见过太多人一上来就设 8 步,结果各种问题排查半天。
想要更多资料,联系可可