数据截至 (上游 commit 5779b17b9a67)
05 · 训练集成与适配器存档
这一章讲什么: 注入完成后怎么训(Trainer、量化基座、梯度检查点的坑),以及训完怎么存、怎么加载、怎么在一台基座上换适配器。存档格式的设计是 PEFT 生态能转起来的关键,值得细看。
1. 训练侧:它是一个普通 nn.Module,但有三个准备动作
1.1 与训练框架的关系
PeftModel 继承 torch.nn.Module,forward 最终落到基座模型——所以 transformers Trainer、accelerate、甚至手写训练循环都不需要知道 PEFT 的存在。优化器怎么知道只更新适配器?不需要知道:基座参数已 requires_grad=False(第 2 章 §4.3),model.parameters() 传给优化器即可。
训练前常看一眼参数占比:print_trainable_parameters(src/peft/peft_model.py:960)打印 trainable params / all params / trainable%。计数函数 get_nb_trainable_parameters(src/peft/peft_model.py:930)里有两个工程补丁:DeepSpeed ZeRO-3 下空壳参数按 ds_numel 计数;bnb 的 Params4bit 要乘回位宽折算(src/peft/peft_model.py:939-952)。
1.2 梯度检查点:冻结模型的特有坑
梯度检查点(gradient checkpointing)靠「重算激活」省显存,但前提是输入张量要求梯度。基座全冻结后,某些层的输入不再携带梯度,反向链会断。prepare_model_for_gradient_checkpointing(src/peft/peft_model.py:720)的修法是给输入嵌入层挂一个 hook,强制输出 requires_grad=True(make_inputs_require_grad,src/peft/peft_model.py:736-741)。PeftModel.__init__ 里检测模型已开梯度检查点会自动调用它(src/peft/peft_model.py:183-184)。
1.3 kbit 基座:QLoRA 的准备仪式
量化加载(8bit/4bit)的模型要微调,官方仪式是 prepare_model_for_kbit_training(src/peft/utils/other.py:158),做四件事:
| 步骤 | 干什么 | 位置 |
|---|---|---|
| 冻结 | 全模型 requires_grad=False | src/peft/utils/other.py:193-195 |
| 升精度 | 非量化参数从 fp16/bf16 升 fp32(LayerNorm 等数值敏感件) | src/peft/utils/other.py:197-209 |
| 清缓存 | 大批量升精度后 empty_cache,释放共享内存设备上的保留显存(注释引 issue #3265) | src/peft/utils/other.py:211-219 |
| 检查点兼容 | 开启梯度检查点并接 §1.2 的 hook;use_reentrant=False 时可跳过 | src/peft/utils/other.py:221-229 |
GPTQ/AQLM/HQQ/torchao 等量化方式被显式排除在「升精度」之外(src/peft/utils/other.py:183-188 逐一判定)——它们的权重有自己的存储格式,不能当普通浮点参数动。
2. 存档:两个文件,名字不属于存档
2.1 存什么
save_pretrained(src/peft/peft_model.py:237)为每个被选中的适配器写两个文件(文件名常量在 src/peft/utils/save_and_load.py:399-401):
| 文件 | 内容 |
|---|---|
adapter_config.json | 序列化的 PeftConfig:方法类型、r、target_modules、base_model_name_or_path…… |
adapter_model.safetensors | 仅适配器权重(以及 modules_to_save 指定的整层) |
基座模型一个字节都不存——这就是适配器 checkpoint 只有几 MB~几百 MB 的原因,也是「离开基座就是废物」的出处(基座名记在 config 里,加载时凭它找回原模型)。
2.2 权重挑选与「剥名」
挑出哪些权重由各 tuner 自己声明:get_peft_model_state_dict(src/peft/utils/save_and_load.py:93)先按 adapter 名过滤掉其他适配器,再委托给 tuner 类的 _get_adapter_state_dict(src/peft/utils/save_and_load.py:172-179),最后处理 modules_to_save 包装的整层(src/peft/utils/save_and_load.py:182-197)。
然后是整个格式里最巧的一步——把 adapter 名从 key 里删掉:
e.g. if the adapter name is
'default'and the original key is'model.q_proj.lora_A.default.weight', the returned key will be'model.q_proj.lora_A.weight'.
(docstring,src/peft/utils/save_and_load.py:106-109;剥名动作在 :279-282,委托给 tuner_cls._remove_adapter_name_from_key)。这样存档里的名字与训练时的 adapter 名解耦:加载时叫什么都行。
2.3 embedding 层的 auto 逻辑
save_embedding_layers="auto" 时会自动判断要不要把 embedding 整层也存上:目标模块里含 embedding 名就存;或者词表大小和基座 config 对不上(训练中途 resize 过词表)也必须存,否则加载回去形状对不上(src/peft/utils/save_and_load.py:229-265)。为拿基座词表它甚至会去 Hub 查 config 文件,离线环境下查不到就保守假设没改(src/peft/utils/save_and_load.py:240-249)。
3. 加载:按名字把权重插回去
3.1 主路径
PeftModel.from_pretrained(base_model, "path-or-hub-id")
① 读 adapter_config.json,按 peft_type 实例化对应 config peft_model.py:490
② 按 config 注入一套空适配器(结构先行)
③ load_peft_weights 解析 safetensors/bin 文件名 save_and_load.py:585
④ set_peft_model_state_dict:key 里重新插入 adapter 名 save_and_load.py:454
→ load_result 报告 missing/unexpected keys
from_pretrained(src/peft/peft_model.py:416)默认 is_trainable=False——加载来推理的适配器默认冻结,要继续训练需显式传 is_trainable=True(src/peft/peft_model.py:446-447)。
给已有 PeftModel 追加第二个适配器用 load_adapter(src/peft/peft_model.py:1393);运行时切换生效对象用 set_adapter(src/peft/peft_model.py:1572);只换权重不换结构用 hotswap_adapter(src/peft/utils/hotswap.py:613,仅 LoRA)。
3.2 架构漂移的换算
基座模型的上游架构会改名(transformers v5 调整了若干模块名)。加载时若模型带 _checkpoint_conversion_mapping,会先把 adapter 的 key 按同样规则换算(src/peft/peft_model.py:498-509);set_peft_model_state_dict 内部还有一道 v5 换算(src/peft/utils/save_and_load.py:493-500)。v4 训的 LoRA 可以加载进 v5 的模型,这两层换算是保证。
4. 关键细节与坑
- 存档不随 DDP/FSDP 包装。
get_peft_model_state_dict的 docstring 明说:分布式训练下要传解开包装的model.module(src/peft/utils/save_and_load.py:112-114)。 adapter_config.json里的基座名可能过时。 换基座加载时get_peft_model会警告名字不符(第 2 章 §3.2)——忽视它等于给适配器配错参照系。- 低显存加载的权重在 meta 设备上。 用
low_cpu_mem_usage=True注入后必须加载真实权重再训练;set_peft_model_state_dict的同名参数要配套使用(docstring 警告,src/peft/utils/save_and_load.py:474-477)。 - 推理时想临时关掉适配器:
disable_adapter上下文(src/peft/peft_model.py:1021)。对 prompt 方法直接不拼;对 LoRA 走第 3 章 forward 的禁用分支,输出精确等于裸基座。 - 一个 batch 混用多适配器:前向传
adapter_names=[...](逐样本指定)。它是 PEFT 私有参数,外层 forward 会先弹掉再传给基座(special_peft_forward_args,src/peft/peft_model.py:164)。 - examples/ 目录是最佳样例库:
examples/下有各方法的可跑训练脚本(因果 LM、序列分类、图像分类等),比文档更贴近当前 commit 的真实 API。
5. 代码地图(本章涉及)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| kbit 训练准备 | src/peft/utils/other.py | prepare_model_for_kbit_training |
| 梯度检查点兼容 | src/peft/peft_model.py | PeftModel.prepare_model_for_gradient_checkpointing |
| 参数统计 | 同上 | PeftModel.get_nb_trainable_parameters、print_trainable_parameters |
| 保存入口 | 同上 | PeftModel.save_pretrained |
| 权重挑选与剥名 | src/peft/utils/save_and_load.py | get_peft_model_state_dict、set_peft_model_state_dict、load_peft_weights |
| 文件名常量 | 同上 | CONFIG_NAME、SAFETENSORS_WEIGHTS_NAME |
| 加载入口 | src/peft/peft_model.py | PeftModel.from_pretrained |