2. ZeRO Stage 2 梯度切分
任务目标
本案例使用 DeepSpeed 的 ZeRO Stage 2 在多卡(2×GPU)环境下训练一个小型 Transformer 模型,验证梯度按数据并行度切分后,单卡梯度内存占用显著低于未启用 ZeRO 的 DDP 基线。
环境与依赖
- Python ≥ 3.8,PyTorch ≥ 1.13,DeepSpeed ≥ 0.9.0,CUDA 可用且至少 2 张 GPU
- 安装:
pip install deepspeed torch
实现与关键代码
以下代码定义了一个小型 Transformer 分类模型,使用 DeepSpeed 引擎启动训练。核心逻辑:通过 zero_optimization.stage = 2 启用 ZeRO Stage 2,梯度在 backward 后按 rank 切分,每个 rank 只保留 1/2 的梯度分片。代码同时打印每步的梯度内存峰值,用于对比验证。
# train_zeRO_stage2.py
import torch
import torch.nn as nn
import deepspeed
import os
# ---- 小型 Transformer 模型(仅用于演示内存变化) ----
class SmallTransformer(nn.Module):
def __init__(self, vocab_size=1000, d_model=128, nhead=4, num_layers=2):
super().__init__()
self.embed = nn.Embedding(vocab_size, d_model)
layer = nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward=512, batch_first=True)
self.encoder = nn.TransformerEncoder(layer, num_layers)
self.head = nn.Linear(d_model, 10)
def forward(self, x):
x = self.embed(x)
x = self.encoder(x)
return self.head(x.mean(dim=1))
# ---- 训练配置 ----
def create_config(use_stage2):
if use_stage2:
return {
"train_batch_size": 16,
"gradient_accumulation_steps": 1,
"zero_optimization": {
"stage": 2,
"reduce_bucket_size": 5e7,
"reduce_scatter": True
},
"fp16": {"enabled": True},
"optimizer": {
"type": "AdamW",
"params": {"lr": 1e-4}
}
}
else:
# DDP 基线(无 ZeRO)
return {
"train_batch_size": 16,
"gradient_accumulation_steps": 1,
"fp16": {"enabled": True},
"optimizer": {
"type": "AdamW",
"params": {"lr": 1e-4}
}
}
def train(use_stage2):
model = SmallTransformer()
config = create_config(use_stage2)
engine, _, _, _ = deepspeed.initialize(
model=model,
model_parameters=model.parameters(),
config=config
)
criterion = nn.CrossEntropyLoss()
# 模拟输入:batch=8/卡,seq_len=32
for step in range(5):
x = torch.randint(0, 1000, (8, 32), device=engine.device)
y = torch.randint(0, 10, (8,), device=engine.device)
out = engine(x)
loss = criterion(out, y)
engine.backward(loss)
engine.step()
# 打印当前 rank 的梯度内存峰值(MB)
grad_mem = sum(p.grad.numel() * p.grad.element_size() for p in model.parameters() if p.grad is not None) / 1e6
if engine.global_rank == 0:
print(f"step={step} loss={loss.item():.4f} grad_mem_rank0={grad_mem:.2f}MB")
torch.distributed.destroy_process_group()
if __name__ == "__main__":
import sys
use_stage2 = sys.argv[1] == "stage2" if len(sys.argv) > 1 else True
train(use_stage2)
运行方式
# 启动 ZeRO Stage 2 训练(2卡)
deepspeed --num_gpus=2 train_zeRO_stage2.py stage2
# 启动 DDP 基线(2卡,无 ZeRO)
deepspeed --num_gpus=2 train_zeRO_stage2.py baseline
验证方法与判定标准
- 观察输出:运行 stage2 时,每步打印的
grad_mem_rank0应约为 baseline 运行时的 1/2(因为梯度被切分到 2 个 rank)。 - 通过条件:stage2 模式下 rank0 的梯度内存峰值显著低于 baseline(例如 baseline 为 40MB 时,stage2 约为 20MB),且 loss 正常下降,无报错。
- 注意:实际数值取决于模型大小与 batch,但比例关系应接近 1/N(N 为 GPU 数)。
常见问题
- 若显存不足,可减小
d_model或batch至 4。 - 确保
deepspeed命令可用,且torch.cuda.device_count() ≥ 2。 - ZeRO Stage 2 的梯度切分仅在 backward 后生效,因此打印需在
backward之后、step之前或之后均可。