与 HuggingFace Trainer 集成的源码路径
8.1 为什么 HF Trainer 用户"看不见 DeepSpeed"
第 3 章演示了"加一行 deepspeed: ds_config.json 就开 ZeRO"。在 LLaMA-Factory(见 07-llama-factory ch10)里更夸张:YAML 只写路径,剩下的几乎透明。本章打开这个"魔法箱子"。
8.2 入口:TrainingArguments.deepspeed
HF Trainer 的入口是 TrainingArguments.deepspeed 字段。它接受三种形式:
| 值的类型 | 意思 |
|---|---|
| None(默认) | 不启用 |
| str(文件路径) | 读 JSON 配置 |
| dict | 直接传配置 |
8.3 启动调用栈
从 args 检测 is_deepspeed_zero3_enabled() 等标志"] T1 --> T2["trainer.train()"] T2 --> T3["_inner_training_loop()"] T3 --> T4["accelerator.prepare()
(transformers ≥ 4.30 走 accelerate)"] T4 --> T5["integrations/deepspeed.py · deepspeed_init()"] T5 --> T6["HfDeepSpeedConfig:
把 'auto' 字段替换为 args 的值"] T6 --> T7["调 deepspeed.initialize(model,
optimizer, lr_scheduler, config_params=...)"] T7 --> T8["返回 model_engine, optimizer,
train_dataloader, lr_scheduler"] T8 --> T9[训练循环: 每步 model_engine.backward + step]
关键:HF 维护 HfDeepSpeedConfig 这个适配器类,它做了两件事:
- "auto" 填充:把 ds_config.json 里所有写成
"auto"的字段,用 TrainingArguments 对应值替换; - 互斥校验:和 args 比对,确保
bf16、gradient_checkpointing、per_device_train_batch_size等没冲突。
8.4 "auto" 字段对照表
| ds_config.json 中 | 取自 TrainingArguments |
|---|---|
train_micro_batch_size_per_gpu = "auto" | per_device_train_batch_size |
gradient_accumulation_steps = "auto" | gradient_accumulation_steps |
train_batch_size = "auto" | micro × accum × world_size |
gradient_clipping = "auto" | max_grad_norm |
bf16.enabled = "auto" | bf16 或 bf16_full_eval |
fp16.enabled = "auto" | fp16 或 fp16_full_eval |
optimizer.params.lr = "auto" | learning_rate |
optimizer.params.weight_decay = "auto" | weight_decay |
optimizer.params.betas = "auto" | (adam_beta1, adam_beta2) |
scheduler.params.total_num_steps = "auto" | 由 Trainer 计算(dataset 长度 × epochs / batch) |
scheduler.params.warmup_num_steps = "auto" | warmup_steps 或 warmup_ratio × total |
"auto" 越多越好:让 Trainer 推算,避免不一致。
8.5 谁定义 optimizer 与 scheduler
有两种模式:
| 模式 | 谁定义 optimizer | 谁定义 scheduler |
|---|---|---|
| DS 定义(推荐) | ds_config.json 写 "optimizer":{...} | 同上 "scheduler":{...} |
| HF 定义 | 不在 ds_config.json 写,由 Trainer 创建 | 同上 |
| 混合 | ds_config 写 optimizer,scheduler 留给 Trainer | 反之亦然 |
三种都合法,但实战推荐让 ds_config 控 optimizer,HF 控 scheduler:
- optimizer 走 DS:能用 cpu_adam / fused_adam 等加速;
- scheduler 走 HF:写
warmup_ratio: 0.1,比 ds_config 里换算 steps 简单。
8.6 启动方式 = torchrun 或 deepspeed
| 命令 | 说明 |
|---|---|
torchrun --nproc_per_node 4 train.py | HF 推荐;Trainer 内部检测 deepspeed 并接管 |
deepspeed --num_gpus 4 train.py | DS 原生 launcher;等效 |
accelerate launch ... train.py | 用 accelerate config 时 |
对一份用 HF Trainer 的脚本,三种 launcher 都能跑出相同结果,因为它们都只负责把环境变量准备好,初始化 process group 的工作在 deepspeed.initialize() 完成。
8.7 LLaMA-Factory 是怎么用的
串起来看:
deepspeed: examples/deepspeed/ds_z3_config.json"] YAML --> Lf["llamafactory-cli train xxx.yaml"] Lf --> Launch["launcher.py 检测
deepspeed 字段 → 强制 torchrun"] Launch --> TR["torchrun → train.workflow.run_exp()"] TR --> TA["TrainingArguments(
deepspeed='examples/.../ds_z3_config.json',
...其他 args...)"] TA --> Hf["transformers.Trainer
检测 deepspeed → 调 deepspeed_init()"] Hf --> DI["deepspeed.initialize()"] DI --> Eng[DeepSpeedEngine 训练循环]
所以你看 LLaMA-Factory 一份 SFT YAML,从应用层到 GPU 上的真正 backward,跨过了 4 个层次:YAML → CLI → Trainer → DS。这层叠并不是冗余 —— 每一层都给上层屏蔽了一些"不该让用户操心"的细节。
8.8 ZeRO-3 + HF 模型保存的特殊处理
ZeRO-3 里每张卡只有 1/N 的参数,model.state_dict() 拿到的不是完整模型。HF Trainer 在保存时会自动调:
self.deepspeed.save_16bit_model(output_dir, "pytorch_model.bin")
它会触发跨卡的 all-gather,把所有参数收集到 rank 0 然后保存。这就是为什么 ds_config 里 ZeRO-3 段必须有:
"stage3_gather_16bit_weights_on_model_save": true
不开的话保存会失败 / 保存出来的是 shard。
8.9 多 epoch / 多 rank 时的 dataloader sampler
HF Trainer 会自动给 train dataset 套 DistributedSampler(或者 ZeRO-3 时可能用 ZeroDistributedSampler)。常踩的坑:
- 自定义 collate_fn 时返回的 dict 必须包含
labels,否则 Trainer 不知道怎么算 loss; - shuffle 由 Sampler 控制,不要在 Dataset 内部自己再 shuffle;
- 跨 epoch 间要让 sampler.set_epoch(epoch),HF Trainer 内部已经做了,但自己写 loop 时记得调。
8.10 当 ds_config 与 TrainingArguments 冲突
HF 的处理优先级(从高到低):
- 显式不写 "auto" 的 ds_config 字段(强约束,Trainer 不会覆盖);
- "auto" 字段 → 从 TrainingArguments 取;
- 什么都没写 → DS 默认值。
例如:
| ds_config.json | TrainingArguments | 实际生效 |
|---|---|---|
"bf16": { "enabled": "auto" } | bf16=True | bf16 开启 |
"bf16": { "enabled": true } | bf16=False | 报错冲突 |
| 不写 bf16 字段 | bf16=True | 不开(DS 默认 fp16) |
所以推荐:能写 "auto" 就写 "auto"。
8.11 自己写脚本而不用 HF Trainer 时
HF Trainer 集成的所有事都是 deepspeed.initialize() 完成的;你完全可以自己手写:
import json
from transformers import AutoModelForCausalLM
import deepspeed
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-4B-Instruct-2507", torch_dtype="bfloat16")
ds_config = json.load(open("ds_config.json"))
# 把"auto"字段自己填了,或者就用具体数值
engine, opt, _, sched = deepspeed.initialize(
model=model,
model_parameters=filter(lambda p: p.requires_grad, model.parameters()),
config=ds_config,
)
# 然后写训练循环
这是不依赖 transformers Trainer 的"原生 DS"用法。后续第 9 章讲的 3D 并行 / Pipeline 也常常走这条路。
8.12 跟进 HfDeepSpeedConfig 的实现
HF 这边的"魔法" 落在 transformers/src/transformers/integrations/deepspeed.py。不在本仓库内,但代码很短,理解一次就好。关键路径(按调用顺序):
Trainer.__init__()
└── 检测 TrainingArguments.deepspeed → 构造 HfTrainerDeepSpeedConfig(ds_config)
└── HfDeepSpeedConfig.__init__ : 把 JSON 读成 dict、记下 stage / dtype
└── 调 trainer_config_process(args) : 把 args 里的 lr / wd / batch ...
塞进 ds_config 里的"占位字段"(凡未写就用 args 值)
trainer.train()
└── trainer._inner_training_loop()
└── accelerator.prepare(model, optimizer, scheduler, dataloader)
└── (accelerate 里的) DeepSpeedPlugin.deepspeed_config_process(args)
└── 把"auto"字段(见 8.4 表)真正替换成 args 值
└── trainer_config_finalize(args, model, num_training_steps)
└── 算 scheduler.total_num_steps / scheduler.warmup_num_steps
└── 把 hidden_size 等模型形状灌给 stage3 的 auto 字段
└── deepspeed.initialize(model, optimizer, ..., config=ds_config)
两个"replace 'auto'"步骤分两次发生,原因是:早期 trainer_config_process() 还不知道总训练步数和模型形状,要等模型 / dataset 都拿到后 trainer_config_finalize() 再补一遍。
8.13 accelerate / DeepSpeedPlugin 与原生 DS
新版 transformers(≥ 4.30)默认走 accelerate 后端。它给 DeepSpeed 包了一层 DeepSpeedPlugin,accelerate config 生成的 yaml 里的几个段对应到 ds_config:
| accelerate yaml 字段 | 对应 ds_config 字段 |
|---|---|
deepspeed_config_file | 整份 ds_config.json 的路径;写了这个,下面字段都被覆盖 |
deepspeed_multinode_launcher | 等价 deepspeed 命令的 --launcher |
zero_stage | zero_optimization.stage |
offload_optimizer_device | zero_optimization.offload_optimizer.device |
offload_param_device | zero_optimization.offload_param.device |
gradient_accumulation_steps | gradient_accumulation_steps |
gradient_clipping | gradient_clipping |
zero3_init_flag | 开 zero.Init() context manager;模型加载时即按 ZeRO-3 分片,省一次完整模型驻留 |
zero3_save_16bit_model | stage3_gather_16bit_weights_on_model_save |
选哪条路?团队经验:
- 纯 DS 训练脚本 → 直接
deepspeedCLI; - HF Trainer → 配
TrainingArguments.deepspeed=path,启动torchrun或deepspeed都行; - accelerate 用户 → 在
accelerate config里指deepspeed_config_file,启动accelerate launch; - LLaMA-Factory / 类似上层框架 → 它们内部其实都走 HF Trainer 这一条,自己只是包了 YAML。
8.14 DS 仓库里自带的 ds_config 模板
装好 DS 后,源码树有几份"开箱可用"的模板,对照着改自己的 config 比从空白写要稳:
| 路径(DS 仓库内) | 用途 |
|---|---|
tests/unit/runtime/zero/... | 单元测试里散落的最小 config,能找到每个字段的极简用法 |
blogs/deepspeed-zero/... | 各篇技术 blog 配套的完整 config |
tests/unit/inference/... | 推理 init_inference 的最小 config |
HF 那一侧(transformers/tests/deepspeed/)也维护着几份代表性的 ds_config_zero{2,3}.json,命名干净,跑 CI 就用它们——非常适合拿来当起点。LLaMA-Factory 仓库下 examples/deepspeed/ 提供 ds_z0/z2/z2_offload/z3/z3_offload_config.json 五件套,本系列书第 7 章 LLaMA-Factory ch10 里有展开。