核心概念:DeepSpeedEngine 与初始化流程
4.1 三个核心抽象
无论你怎么用 DeepSpeed,最后都绕不开这三个对象:
| 对象 | 类(文件) | 角色 |
|---|---|---|
DeepSpeedConfig | runtime/config.py | JSON → Python 的统一表示,做合法性校验 |
DeepSpeedEngine | runtime/engine.py | 包住原 model,提供 .backward / .step |
DeepSpeedZeroOptimizer* | runtime/zero/stage_1_and_2.py、stage3.py | 真正负责"按 stage 切参数 / 梯度" |
本章把这三个东西讲透,下一章再深入 ZeRO 数学细节。
4.2 deepspeed.initialize() 的签名
从 deepspeed/__init__.py 看,签名是这样的(精简):
def initialize(args=None,
model: torch.nn.Module = None,
optimizer=None,
model_parameters=None,
training_data=None,
lr_scheduler=None,
distributed_port=29500,
mpu=None,
dist_init_required=None,
collate_fn=None,
config=None,
mesh_param=None,
config_params=None):
实际项目里用得最多的几个参数:
| 参数 | 含义 |
|---|---|
model | 你自己写的 nn.Module,未经任何 wrapper(不要套 DDP) |
model_parameters | 要优化的参数 iterable,常写 filter(lambda p: p.requires_grad, model.parameters()) |
optimizer | 可选;不传则由 JSON 的 optimizer 字段创建 |
lr_scheduler | 可选;不传则由 JSON 的 scheduler 字段创建 |
training_data | 可选;传了会自动加 DistributedSampler |
config | 字符串路径或 dict;和 args.deepspeed_config 二选一 |
mpu | 模型并行 utility(Megatron 风格的 parallel_state) |
4.3 调用 initialize() 后内部发生了什么
(runtime/config.py)"] A --> B["init_process_group (NCCL)
由 dist_init_required 控制"] B --> Z3init{"是否 ZeRO-3 init
context 已开?"} Z3init -- 是 --> Shut["zero.partition_parameters.shutdown_init_context()"] Z3init -- 否 --> Wrap{"RLHF / 推理模式?"} Shut --> Wrap Wrap -- RLHF --> HE["DeepSpeedHybridEngine
(__init__.py:202)"] Wrap -- 普通训练 --> DE["DeepSpeedEngine
(runtime/engine.py:208)"] DE --> CFGs["5 个 _configure_* 阶段
(见 4.10)"] CFGs --> C{"zero_optimization.stage 是哪个?"} C -- 0/1/2 --> S12["DeepSpeedZeroOptimizer
(stage_1_and_2.py:126)"] C -- 3 --> S3["DeepSpeedZeRoOffload + Stage3
(stage3.py:136 + parameter_offload.py:112)"] S12 --> Hooks["注册 module forward / backward hook
(engine.py:2308–2380)"] S3 --> Hooks Hooks --> Restore["zero.partition_parameters.restore_init_context()"] Restore --> Ret[(model_engine, optimizer,
train_loader, lr_scheduler)]
关键点:
- Process group 初始化由 DS 接管,不需要你自己写
torch.distributed.init_process_group; - JSON 解析失败会立刻抛
pydantic.ValidationError,提示哪一行字段类型不对; - 注册的 hook 是 DeepSpeed 接管反向、梯度同步、参数搬运的入口。
4.4 DeepSpeedEngine 上你需要知道的方法
| 方法 | 等价于 | 说明 |
|---|---|---|
engine(x) | model(x) | 前向;engine 是 nn.Module 子类 |
engine.backward(loss) | loss.backward() | 触发 ZeRO 的 grad partition / all-reduce |
engine.step() | optimizer.step + zero_grad | 真正更新参数(按 accumulation 累够才动) |
engine.save_checkpoint(dir) | — | 保存所有 rank 的 shard |
engine.load_checkpoint(dir) | — | 恢复,自动处理 shard 拼装 |
engine.train()/eval() | 同 PyTorch | 切训练 / 推理模式 |
engine.module | 访问原 model | 保存 HF 格式时需要 |
engine.device | — | 你这张卡的 device,建议直接用 |
engine.local_rank / global_rank | — | 直接拿 rank 信息 |
engine.gradient_accumulation_steps() | — | 动态读取当前累积步数 |
4.5 DeepSpeedConfig:JSON → Python 的桥
runtime/config.py 用 pydantic 把 JSON 各字段映射成 Python class。它在做几件事:
- 类型校验:
stage必须是 0/1/2/3,bf16.enabled必须是 bool; - 互斥检查:
fp16.enabled和bf16.enabled不能同时 true; - "auto" 解析:把
"auto"替换成调用方提供的值(HF Trainer 用这个机制注入 batch size 等); - 互相约束:
zero_optimization.stage=3 + offload_optimizer.device=nvme时检查aioop 是否编译好。
4.6 启动器:deepspeed 命令做了什么
当你写 deepspeed --num_gpus 4 train.py ... 时,deepspeed/launcher/runner.py 会做:
| 步 | 动作 |
|---|---|
| 1 | 解析 --num_gpus / --hostfile / --include / --exclude |
| 2 | 对每台机器,ssh 上去执行 launcher/launch.py(多机时) |
| 3 | launch.py 用 torch.distributed.run 风格拉起 N 个 python 子进程 |
| 4 | 每个子进程注入 LOCAL_RANK / RANK / WORLD_SIZE / MASTER_ADDR / MASTER_PORT |
| 5 | 你的脚本里 deepspeed.initialize() 用这些环境变量 init_process_group |
常见 launcher 参数:
deepspeed \
--num_gpus 8 \ # 单机用多少卡
--num_nodes 2 \ # 多机用几台
--hostfile hostfile \ # 节点列表
--master_port 29500 \ # 主节点 NCCL 端口
--include "worker-0:0,1,2,3" \ # 只用这台机的 0-3 卡
--no_local_rank \ # 不向子进程传 --local_rank
train.py --deepspeed --deepspeed_config ds_config.json
多机训练的难点几乎都在 --hostfile、ssh 免密、防火墙——脚本本身不用动。
4.7 三种 rank:local / global / data parallel
DeepSpeed 比 PyTorch 多了一些概念:
| 名字 | 含义 | 从哪取 |
|---|---|---|
local_rank | 本机内编号,0..gpus_per_node-1 | engine.local_rank |
global_rank | 整个集群编号,0..WORLD_SIZE-1 | engine.global_rank |
data_parallel_world_size | DP 维度的大小 | engine.dp_world_size |
当 ZeRO 单独使用时 dp_world_size == WORLD_SIZE;当和 Megatron 模型并行混用时(详见第 9 章)就不等。
4.8 一份等效"PyTorch-style 心智模型"
新手刚切到 DeepSpeed 经常困惑"我以前那套写法去哪了"。可以这样建立映射:
| 原 PyTorch 概念 | DeepSpeed 等价物 |
|---|---|
DistributedDataParallel(model) | engine = deepspeed.initialize(model=...) |
optimizer.step() | engine.step() |
optimizer.zero_grad() | 自动包含在 engine.step() |
autocast() + GradScaler | "bf16" / "fp16" 配置段 |
torch.save(model.state_dict(), path) | engine.save_checkpoint(dir) |
model.load_state_dict(torch.load(path)) | engine.load_checkpoint(dir) |
| 动态 batch size 调整 | JSON 的 gradient_accumulation_steps |
梯度裁剪 clip_grad_norm_() | JSON 的 gradient_clipping |
4.9 这章给你留下的应该是什么
- 知道
DeepSpeedEngine= "你模型的封装 + ZeRO 优化器 + 通信 hook"; - 能从
engine.module拿到原模型; - 知道
backward / step一定要用 engine 的方法; - 会区分
local_rank、global_rank、dp_world_size; - 看 launcher 命令时知道每个 flag 的角色。
4.10 DeepSpeedEngine.__init__ 的真实构造顺序
翻开 runtime/engine.py,DeepSpeedEngine 的类定义在第 208 行附近,构造函数大约 100 行,内部把工作切成了 5 个 _configure_* 阶段,调用顺序是固定的(弄清这个顺序,调试 NaN / OOM / hang 时就知道在哪里下断点):
| 序 | helper | 行号 | 这一步在做什么 |
|---|---|---|---|
| 1 | _configure_with_arguments(args, mpu) | ~278 | 把 args 里的 local_rank / deepspeed_config 解到 self;处理 mpu(Megatron 风格的 model parallel utility);选 accelerator(CUDA / ROCm / Habana / XPU) |
| 2 | _configure_distributed_model() | ~305 | 把原 model to(self.device);如果开了 ZeRO-3 init context,在这里把参数留在 NOT_AVAILABLE 状态;处理 BF16 cast;初始化 mpu 通信组 |
| 3 | _configure_optimizer(client_optimizer, model_parameters) | ~365 | 若用户传了 optimizer 就包装;否则根据 JSON "optimizer" 段构造(CPUAdam / FusedAdam / Muon / Lion / Lamb / OneBitAdam) |
| 4 | _configure_zero_optimizer(optimizer) | ~370 | 按 stage 包成 DeepSpeedZeroOptimizer 或 DeepSpeedZeroOptimizer_Stage3;如果 stage 0 就跳过 |
| 5 | _configure_bf16_optimizer(optimizer) | ~372 | BF16 时把 optimizer 再包一层 BF16_Optimizer,维护 fp32 master weight |
顺序意义:BF16 wrapper 在 ZeRO wrapper 外面。loss scale / dtype cast 发生在 ZeRO 切分之前看到的张量上。如果你看到一段 stack trace 里出现 BF16_Optimizer.step → DeepSpeedZeroOptimizer.step 的嵌套,就是这个顺序在体现。
4.11 Engine 注册的 hook 总表
DeepSpeed 在初始化时给原 model 挂了若干 hook,这是它能"无感"接管 forward / backward / save 的核心。手写训练循环时调错方法(例如直接 loss.backward())就会跳过这些 hook,导致 ZeRO-3 hang 或梯度对不齐。
| hook | 注册位置 | 作用 |
|---|---|---|
module forward pre-hook (_module_forward_pre_hook) | engine.py:~2308 | 每次 forward 进入 root module 时打 timer、做一次 ZeRO-3 trace 校验 |
module forward post-hook (_module_forward_post_hook) | engine.py:~2309 | 退出 root module 时打 timer;ZeRO-3 释放本步用过且不再用的参数 |
output backward hook (register_output_backward_hooks) | engine.py:~2380 | 在 forward 返回的 loss tensor 上挂;它的回调就是 _backward_prologue_per_tensor(执行 GAS 缩放) |
| TP 一致性检查 hook | engine.py:~566 | 仅 AutoTP 时;检测各 TP rank 输入是否一致 |
ZeROOrderedDict.__getitem__ 拦截 | partition_parameters.py:57–68 | 访问 module._parameters[name] 时若状态 NOT_AVAILABLE 自动 all-gather(细节见 ch05.12) |
register_external_parameter() 入口 | partition_parameters.py:142–192 | 跨 module 用同一个参数(如 tied embedding)时由用户/Trainer 调用,告诉 Coordinator 此参数会被"外部"消费 |
4.12 backward() 的真实路径
当你写 engine.backward(loss) 时,engine.py 第 2615 行附近的代码做了这些(按时间顺序):
- 置位
self._running_engine_backward = True(标记自己进入"DS 内部反向",避免一些 hook 重入); - 调
self.optimizer.scale_if_loss(loss)(ZeRO 路径上会做 loss scale,BF16 时不缩;FP16 时按当前 dynamic loss scale 乘进去); - 真正调
loss.backward(**backward_kwargs);触发的 backward 路径上,每一层的 hook 是 4.11 注册的那些; - 调
self._backward_epilogue():刷 ZeRO bucket、reduce-scatter 梯度、释放参数(stage 3); - 返回
gas_scaled_loss(按梯度累积步数除回去后的 loss,方便你打日志)。
注意:_scale_wrt_gas 标记控制要不要再按 gradient_accumulation_steps 除一次。HF Trainer 会传 scale_wrt_gas=False(因为 Trainer 自己已经按 GAS 缩了 loss),自己写循环时默认是 True。两边都缩就是常见的"loss 莫名变小到 1/4"bug 来源。
4.13 梯度累积边界判定
DeepSpeed 用一个看起来奇怪、其实很合理的公式判定 "这次 step 是不是真要更新参数"(engine.py:~2679):
self._is_gradient_accumulation_boundary = (
(self.micro_steps + 1) % self.gradient_accumulation_steps() == 0
)
关键点:
- 是
micro_steps + 1而不是micro_steps:因为micro_steps在本次 backward 之后才会自增;要在 backward 之前看到"这次累完了没有",必须 +1; - "非边界"的
step()调用只做梯度累积,不真正更新参数也不更新 lr_scheduler; engine._step_applied反映上一次step()是否真的更新过参数——日志里需要"只在真更新的步打 lr"时,用它判 boolean。
4.14 RLHF 场景下的 DeepSpeedHybridEngine
读 deepspeed/__init__.py 第 202 行附近会看到一个分支:
if config.hybrid_engine.enabled:
engine = DeepSpeedHybridEngine(args=args, model=model, ...)
else:
engine = DeepSpeedEngine(args=args, model=model, ...)
DeepSpeedHybridEngine 是给 DeepSpeed-Chat 这类 RLHF 训练用的——它内部维护两套权重视图:
- 训练视图:ZeRO 切分态,按 stage 反向;
- 生成视图:合并 + fused inference kernel,actor 做 rollout 时切到这里跑得快。
切换由 engine.eval()/train() 触发。开关字段是 ds_config.json 里的 "hybrid_engine": { "enabled": true, "inference_tp_size": ..., "release_inference_cache": ... }。如果你不做 RLHF,不需要关心这条分支;但看 issue 时偶尔会看到"Hybrid Engine"这个词,知道是这个意思就够了。