Chapter 08

与 HuggingFace Trainer 集成的源码路径

📌 本章追的是 transformers 主仓的 src/transformers/trainer.py 与 src/transformers/integrations/deepspeed.py 的调用链;DS 这边对应 commit 510ebe58e4e4 。两端版本要"看得见 deepspeed",参考第 2 章版本矩阵。

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 启动调用栈

flowchart TB T0["Trainer(model, args=TrainingArguments(deepspeed=...))"] T0 --> T1["Trainer.__init__:
从 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 这个适配器类,它做了两件事:

  1. "auto" 填充:把 ds_config.json 里所有写成 "auto" 的字段,用 TrainingArguments 对应值替换;
  2. 互斥校验:和 args 比对,确保 bf16gradient_checkpointingper_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" bf16bf16_full_eval
fp16.enabled = "auto" fp16fp16_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_stepswarmup_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

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 是怎么用的

串起来看:

flowchart TB YAML["LLaMA-Factory YAML
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)。常踩的坑:

8.10 当 ds_config 与 TrainingArguments 冲突

HF 的处理优先级(从高到低):

  1. 显式不写 "auto" 的 ds_config 字段(强约束,Trainer 不会覆盖);
  2. "auto" 字段 → 从 TrainingArguments 取;
  3. 什么都没写 → DS 默认值。

例如:

ds_config.jsonTrainingArguments实际生效
"bf16": { "enabled": "auto" }bf16=Truebf16 开启
"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 包了一层 DeepSpeedPluginaccelerate 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

选哪条路?团队经验:

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 里有展开。