跳到主要内容

数据截至 (上游 commit 5779b17b9a67)

02 · 方法注册表与适配器注入

这一章讲什么: get_peft_model(model, LoraConfig(...)) 这一行背后发生的一切——方法怎么注册、入口怎么分流、注入循环怎么按名字找到 q_proj 并把它换掉。这是 PEFT 对任意模型都适用的原因,也是自己加新方法的必读路径。


1. 它要解决的两个小问题

  • 统一入口问题: 库里有 44 种微调方法(src/peft/tuners/ 下每个目录一个),用户不该为每种方法学一套 API。
  • 零侵入问题: transformers 每星期都在发新模型,PEFT 不可能为每个模型写适配代码。注入必须对模型源码零依赖

两个问题的答案分别是:一张注册表按模块名匹配的原地替换


2. 注册表:导入即登记

2.1 名册与注册函数

所有方法的枚举值在 PeftTypesrc/peft/utils/peft_types.py:19),注册函数是 register_peft_methodsrc/peft/utils/peft_types.py:128)。它往四张全局表里填条目(表本身是 src/peft/mapping.py:30-33 的空 dict):

键 → 值
PEFT_TYPE_TO_CONFIG_MAPPINGpeft_type → config 类
PEFT_TYPE_TO_TUNER_MAPPINGpeft_type → tuner(模型级注入类)
PEFT_TYPE_TO_PREFIX_MAPPINGpeft_type → 参数名前缀(如 lora_
PEFT_TYPE_TO_MIXED_MODEL_MAPPINGpeft_type → 可混用的 tuner(子集)

2.2 注册发生在 import 时

每个 tuner 包的 __init__.py 末尾调一次注册。LoRA 的(src/peft/tuners/lora/__init__.py:66):

register_peft_method(name="lora", config_cls=LoraConfig, model_cls=LoraModel, is_mixed_compatible=True)

注册函数本身是严格的前置校验器:名字必须小写、必须先有对应 PeftType 枚举、不允许重名、前缀不许撞车(src/peft/utils/peft_types.py:167-192)。想加新方法 = 新目录 + 枚举值 + 一行注册,框架代码一行不用改——这是典型的插件式注册表模式。


3. get_peft_model:分流器

入口函数 get_peft_modelsrc/peft/mapping_func.py:105)本身不做任何注入,它只做三件事:记录基座名、发警告、按配置分流。

3.1 分流逻辑

get_peft_model(model, peft_config)

├─ mixed=True ─────────► PeftMixedModel (混合多种适配器)

├─ prompt learning ────► PeftModel(按 task_type 选子类)
│ prompt 方法没有 tuner 类,
│ 靠 PeftModel.forward 拼虚拟 token

├─ task_type 不在映射表 ──► 基础 PeftModel (forward 直接透传)

└─ 其余 ───────────────► PeftModelForCausalLM 等
(auto.py 的 task_type → 子类映射)

对应代码是末尾两个 return 分支(src/peft/mapping_func.py:182-203);task 子类映射表在 MODEL_TYPE_TO_PEFT_MODEL_MAPPINGsrc/peft/auto.py:45-52),覆盖 CAUSAL_LMSEQ_2_SEQ_LMSEQ_CLS 等六种任务。

3.2 防御性警告(全是踩坑史)

get_peft_model 函数体的前一半是警告集锦,每条都对应一类真实事故:

警告触发条件位置
二次注入模型里已有 BaseTunerLayer,提示先 .unload()src/peft/mapping_func.py:146-150
基座名变更config 记的 base_model_name_or_path 与当前模型不符src/peft/mapping_func.py:152-156
eva + 非低显存模式init_lora_weights="eva"low_cpu_mem_usage=Falsesrc/peft/mapping_func.py:165-173
adapter 名含前缀adapter 名会干扰加载时的权重识别src/peft/mapping_func.py:175-180

4. 注入主循环:BaseTuner.inject_adapter

真正动模型的是 tuner 类。PeftModel.__init__ 按注册表实例化 tuner 并让它包住原模型(src/peft/peft_model.py:171-176);tuner 构造时调 inject_adaptersrc/peft/tuners/tuners_utils.py:795)。

4.1 五步走

inject_adapter(model, adapter_name)
① 前置校验与配置准备 _check_new_adapter_config / _prepare_adapter_config
② 展开 "all-linear" _maybe_include_all_linear_layers
③ 遍历 named_modules,逐层判定:
命中? ──► _create_and_replace(原地替换)
排除? ──► 记入 excluded_modules
④ 兜底检查:一个都没命中 → NoMatchingPeftModuleError
⑤ _mark_only_adapters_as_trainable:冻结基座全部参数

几个值得知道的机制:

  • target_modules=None 时的自动推断。 _prepare_adapter_configsrc/peft/tuners/tuners_utils.py:385-414)按 model_type 查内置映射表,如 llama → ["q_proj", "v_proj"]。LoRA 的表在 TRANSFORMERS_MODELS_TO_LORA_TARGET_MODULES_MAPPINGsrc/peft/utils/constants.py:67),t5/bart/gpt2 等各有默认目标。
  • target_modules="all-linear" 常量 INCLUDE_LINEAR_LAYERS_SHORTHANDsrc/peft/utils/constants.py:404),由 _maybe_include_all_linear_layerssrc/peft/tuners/tuners_utils.py:2390)展开成模型里全部 nn.Linear/Conv1D 的名字——这个做法直接注明移植自 QLoRA 仓库。
  • 目标列表压缩优化。 目标名超过 20 个(MIN_TARGET_MODULES_FOR_OPTIMIZATIONsrc/peft/utils/constants.py:413)时,用 _find_minimal_target_modules 求能区分「目标/非目标」的最小集合,因为匹配是 O(目标数 × 模块数)(src/peft/tuners/tuners_utils.py:887-910,注释里挂了 diffusers issue #9297 的出处;IA³ 因 feedforward_modules 耦合被显式排除)。
  • 也能按 state_dict 注入。 不知道确切配置时,传一份 checkpoint 的 state_dict,只看 key 里的 lora_ 前缀推断哪些层被适配过(src/peft/tuners/tuners_utils.py:923-927),并且会和 config 匹配结果对照、不一致就发 RuntimeWarningsrc/peft/tuners/tuners_utils.py:1009-1030)。

4.2 匹配语义:check_target_module_exists

逐层判定在 check_target_module_existssrc/peft/tuners/tuners_utils.py:2301)。匹配规则按优先级:

规则语义位置
exclude_modules先排雷:命中的直接标 _ExcludedModulesrc/peft/tuners/tuners_utils.py:2314-2321
modules_to_save永不注入——全量训练它和挂适配器行为必冲突src/peft/tuners/tuners_utils.py:2323-2328
字符串 target_modules当作正则 fullmatchsrc/peft/tuners/tuners_utils.py:2334-2335
列表 target_modules精确等值,或模块名以 .<target> 结尾src/peft/tuners/tuners_utils.py:2336-2340
layers_to_transform在命中的基础上再按层号过滤;层号用懒惰正则取第一段数字,注释特别说明贪婪写法会在 MoE 上误取 expert 序号src/peft/tuners/tuners_utils.py:2342-2370

「结尾匹配」是默认体验的来源:写 "q_proj" 就能命中所有层的 model.layers.N.self_attn.q_proj

4.3 替换与冻结

命中后调 LoraModel._create_and_replacesrc/peft/tuners/lora/model.py:219):

  • 先解析 rank_pattern/alpha_pattern 的逐层覆盖(src/peft/tuners/lora/model.py:234-237)。
  • 已包过 LoRA 的层不再重包,只在其上 update_layer 加新适配器(src/peft/tuners/lora/model.py:280-290)——多适配器共用一个包装。
  • 新层由 _create_new_module 按基座类型分发创建(量化基座的分发链见第 3 章),再由 _replace_module setattr 回父模块并把适配器权重搬到基座所在设备(src/peft/tuners/lora/model.py:365-391,含对 bnb qweight 等特殊权重名的探测)。

最后 _mark_only_adapters_as_trainablesrc/peft/tuners/tuners_utils.py:502)把不在适配器前缀下的参数全部 requires_grad=False。注意它是结构化判定(收集 tuner 层的 state_dict 前缀)而不是按名字模糊匹配——注释明说,为了防止基座里恰好名字含 lora_ 的参数被误判(src/peft/tuners/tuners_utils.py:506-519)。bias="all"/"lora_only" 两种例外在这里处理(src/peft/tuners/tuners_utils.py:521-535)。

4.4 原理演示

用 20 行代码演一遍「按名匹配 + 原地替换」的核心想法(# 示意,非源码):

import torch.nn as nn

def inject(model: nn.Module, target_names: set[str]): # 示意,非源码
for name, module in list(model.named_modules()):
if not any(name.endswith(f".{t}") or name == t for t in target_names):
continue
parent_name, _, child_name = name.rpartition(".")
parent = model.get_submodule(parent_name) if parent_name else model
setattr(parent, child_name, LoRALinear(module)) # 包装后放回
for n, p in model.named_parameters():
if "lora_" not in n: # 冻结非适配器参数
p.requires_grad = False

重点看:全程没有碰模型类的定义——只在实例的模块树上做 setattr。这就是 PEFT 能跟上 transformers 发版速度的原因。


5. 关键细节与坑

  • 一个都没命中 = 硬报错,不是静默通过。 NoMatchingPeftModuleError 按三种情形给出不同文案:全部被 exclude_modules 排掉 / 全部不匹配 / 两者混合,并提示检查 layers_to_transformsrc/peft/tuners/tuners_utils.py:1032-1067)。写错模块名是新手第一大坑。
  • exclude_modules 写错了会提醒。 传了排除名单但一个都没用上,注入结束后会发警告(src/peft/tuners/tuners_utils.py:1069-1074)。
  • adapter 名不是命名空间装饰,是数据布局。 它进参数 key(lora_A.default.weight)、进 checkpoint 格式、进多适配器调度;起名避开方法前缀(§3.2 最后一条警告)。
  • 不想被 PeftModel 包装可以用裸注入。 inject_adapter_in_modelsrc/peft/mapping.py:47)直接在原模型上注入并返回原模型,适合 diffusers 这类非 HF 任务体系;但它明确不支持 prompt learning 和 adaption prompt(src/peft/mapping.py:77-78)。
  • transformers v5 架构改名有适配层。 注入开头会把旧版 config 的目标名换算到新架构(convert_peft_config_for_transformerssrc/peft/tuners/tuners_utils.py:829-845),保证 v4 时代训的 checkpoint 还能注入 v5 模型。

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

主题文件路径符号名
方法枚举与注册src/peft/utils/peft_types.pyPeftTyperegister_peft_method
注册表本体src/peft/mapping.pyPEFT_TYPE_TO_CONFIG_MAPPINGinject_adapter_in_model
统一入口src/peft/mapping_func.pyget_peft_model
task 分流src/peft/auto.pyMODEL_TYPE_TO_PEFT_MODEL_MAPPING
注入主循环src/peft/tuners/tuners_utils.pyBaseTuner.inject_adapter
匹配判定同上check_target_module_exists_maybe_include_all_linear_layers_find_minimal_target_modules
LoRA 的替换逻辑src/peft/tuners/lora/model.pyLoraModel._create_and_replaceLoraModel._create_new_module
冻结src/peft/tuners/tuners_utils.pyBaseTuner._mark_only_adapters_as_trainable