Chapter 04

核心概念:DeepSpeedEngine 与初始化流程

📌 源码引用基于 deepspeed/__init__.py 、 deepspeed/runtime/engine.py 、 deepspeed/runtime/config.py · commit 510ebe58e4e4 。

4.1 三个核心抽象

无论你怎么用 DeepSpeed,最后都绕不开这三个对象:

对象类(文件)角色
DeepSpeedConfig runtime/config.py JSON → Python 的统一表示,做合法性校验
DeepSpeedEngine runtime/engine.py 包住原 model,提供 .backward / .step
DeepSpeedZeroOptimizer*runtime/zero/stage_1_and_2.pystage3.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() 后内部发生了什么

flowchart TB Call["deepspeed.initialize(model, ...)"] Call --> A["DeepSpeedConfig(json) 解析 + 校验
(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)]

关键点:

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。它在做几件事:

  1. 类型校验stage 必须是 0/1/2/3,bf16.enabled 必须是 bool;
  2. 互斥检查fp16.enabledbf16.enabled 不能同时 true;
  3. "auto" 解析:把 "auto" 替换成调用方提供的值(HF Trainer 用这个机制注入 batch size 等);
  4. 互相约束zero_optimization.stage=3 + offload_optimizer.device=nvme 时检查 aio op 是否编译好。

4.6 启动器:deepspeed 命令做了什么

当你写 deepspeed --num_gpus 4 train.py ... 时,deepspeed/launcher/runner.py 会做:

动作
1解析 --num_gpus / --hostfile / --include / --exclude
2对每台机器,ssh 上去执行 launcher/launch.py(多机时)
3launch.pytorch.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_sizeDP 维度的大小 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 这章给你留下的应该是什么

4.10 DeepSpeedEngine.__init__ 的真实构造顺序

翻开 runtime/engine.pyDeepSpeedEngine 的类定义在第 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)~370stage 包成 DeepSpeedZeroOptimizerDeepSpeedZeroOptimizer_Stage3;如果 stage 0 就跳过
5_configure_bf16_optimizer(optimizer)~372BF16 时把 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 行附近的代码做了这些(按时间顺序):

  1. 置位 self._running_engine_backward = True(标记自己进入"DS 内部反向",避免一些 hook 重入);
  2. self.optimizer.scale_if_loss(loss)(ZeRO 路径上会做 loss scale,BF16 时不缩;FP16 时按当前 dynamic loss scale 乘进去);
  3. 真正调 loss.backward(**backward_kwargs);触发的 backward 路径上,每一层的 hook 是 4.11 注册的那些;
  4. self._backward_epilogue():刷 ZeRO bucket、reduce-scatter 梯度、释放参数(stage 3);
  5. 返回 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
)

关键点:

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 训练用的——它内部维护两套权重视图:

切换由 engine.eval()/train() 触发。开关字段是 ds_config.json 里的 "hybrid_engine": { "enabled": true, "inference_tp_size": ..., "release_inference_cache": ... }。如果你不做 RLHF,不需要关心这条分支;但看 issue 时偶尔会看到"Hybrid Engine"这个词,知道是这个意思就够了。