跳到主要内容

数据截至 (上游 commit 5779b17b9a67)

05 · 训练集成与适配器存档

这一章讲什么: 注入完成后怎么训(Trainer、量化基座、梯度检查点的坑),以及训完怎么存、怎么加载、怎么在一台基座上换适配器。存档格式的设计是 PEFT 生态能转起来的关键,值得细看。


1. 训练侧:它是一个普通 nn.Module,但有三个准备动作

1.1 与训练框架的关系

PeftModel 继承 torch.nn.Moduleforward 最终落到基座模型——所以 transformers Trainer、accelerate、甚至手写训练循环都不需要知道 PEFT 的存在。优化器怎么知道只更新适配器?不需要知道:基座参数已 requires_grad=False(第 2 章 §4.3),model.parameters() 传给优化器即可。

训练前常看一眼参数占比:print_trainable_parameterssrc/peft/peft_model.py:960)打印 trainable params / all params / trainable%。计数函数 get_nb_trainable_parameterssrc/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_checkpointingsrc/peft/peft_model.py:720)的修法是给输入嵌入层挂一个 hook,强制输出 requires_grad=Truemake_inputs_require_gradsrc/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_trainingsrc/peft/utils/other.py:158),做四件事:

步骤干什么位置
冻结全模型 requires_grad=Falsesrc/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_pretrainedsrc/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_dictsrc/peft/utils/save_and_load.py:93)先按 adapter 名过滤掉其他适配器,再委托给 tuner 类的 _get_adapter_state_dictsrc/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_pretrainedsrc/peft/peft_model.py:416)默认 is_trainable=False——加载来推理的适配器默认冻结,要继续训练需显式传 is_trainable=Truesrc/peft/peft_model.py:446-447)。

给已有 PeftModel 追加第二个适配器用 load_adaptersrc/peft/peft_model.py:1393);运行时切换生效对象用 set_adaptersrc/peft/peft_model.py:1572);只换权重不换结构用 hotswap_adaptersrc/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.modulesrc/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_argssrc/peft/peft_model.py:164)。
  • examples/ 目录是最佳样例库examples/ 下有各方法的可跑训练脚本(因果 LM、序列分类、图像分类等),比文档更贴近当前 commit 的真实 API。

5. 代码地图(本章涉及)

主题文件路径符号名
kbit 训练准备src/peft/utils/other.pyprepare_model_for_kbit_training
梯度检查点兼容src/peft/peft_model.pyPeftModel.prepare_model_for_gradient_checkpointing
参数统计同上PeftModel.get_nb_trainable_parametersprint_trainable_parameters
保存入口同上PeftModel.save_pretrained
权重挑选与剥名src/peft/utils/save_and_load.pyget_peft_model_state_dictset_peft_model_state_dictload_peft_weights
文件名常量同上CONFIG_NAMESAFETENSORS_WEIGHTS_NAME
加载入口src/peft/peft_model.pyPeftModel.from_pretrainedPeftModel.load_adapter
config 存取src/peft/config.pyPeftConfigMixin.save_pretrainedPeftConfigMixin.from_pretrained
热插拔src/peft/utils/hotswap.pyhotswap_adapter