跳到主要内容

数据截至 (上游 commit 5779b17b9a67)

PEFT — 架构与原理

30 秒导读: PEFT 是 HuggingFace 的参数高效微调库。它解决的问题是:想微调一个几十亿参数的模型,但显存和算力只够训很小一部分参数。PEFT 的做法是把基座模型冻结,只在其内部插入少量可训练参数(LoRA 低秩矩阵、虚拟 prompt 等),44 种方法共用同一个 get_peft_model(model, config) 入口。训出来的「适配器」通常只有几 MB 到几百 MB,可以独立保存、分享,甚至合并回原模型让推理零开销。


1. 这是什么(零基础也能懂)

一句话定义

PEFT 是一个 PyTorch 库:给它一个预训练模型和一份「微调方法配置」,它把模型改造成「原模型冻结 + 新增一小撮可训练参数」的形态,训练完只保存新增的那部分参数。

它要解决谁的什么问题

假设你有一张 24GB 的消费级显卡,想让 Qwen2.5-3B 学会你的领域知识:

  • 全量微调要更新全部 30 亿参数,优化器状态(如 Adam)还要再存两份动量,显存直接爆掉。
  • PEFT 路线(以 LoRA 为例)只给注意力层挂上秩 16 的低秩矩阵对,可训练参数约 370 万——占 0.12%,一张卡就能训。

这就是官方 README 里的真实数字:trainable params: 3,686,400 || all params: 3,089,625,088 || trainable%: 0.1193README.md Quickstart 节)。

它能做什么

能力说明
44 种微调方法每种方法是 src/peft/tuners/ 下一个目录:LoRA、AdaLoRA、IA³、Prompt/P-/Prefix-tuning、BoFT、VeRA、DoRA……
任意 nn.Module不限于 transformers 模型;只是 task_type 相关功能要求模型遵循 transformers 约定(src/peft/mapping_func.py:119-120 docstring)
量化基座上微调与 bitsandbytes 8bit/4bit 配合即 QLoRA 路线,还有 GPTQ/AWQ/HQQ/torchao 等后端的分发器
多适配器一个基座挂多个适配器,运行时切换、混合、加权融合(add_weighted_adapter
权重合并多数方法可把适配器权重并回基座,得到一个普通模型,推理无任何额外开销
适配器存档只存适配器参数 + 一个 adapter_config.json,天然适配 Hub 分享

用起来什么样

最小 LoRA 微调(摘自 README.md Quickstart):

from transformers import AutoModelForCausalLM
from peft import LoraConfig, TaskType, get_peft_model

model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-3B-Instruct", device_map="cuda")
peft_config = LoraConfig(r=16, lora_alpha=32, task_type=TaskType.CAUSAL_LM)
model = get_peft_model(model, peft_config) # 原地改造:冻结基座 + 注入 LoRA 层
model.print_trainable_parameters() # trainable%: 0.1193

# ... 用 transformers Trainer 正常训练 ...
model.save_pretrained("qwen2.5-3b-lora") # 只保存 LoRA 权重 + adapter_config.json

推理时加载:PeftModel.from_pretrained(base_model, "qwen2.5-3b-lora"),或者先 merge_and_unload() 得到一个不带任何适配器痕迹的普通模型。

一句话直觉

把大模型想成一套精装房。 全量微调是拆墙重盖;LoRA 是在墙上并联一条很窄的「旁路管线」——主干不动,旁路承担「这个任务与通用行为的差」。房子(基座权重)还是那套房子,旁路(适配器)可以拆下来带走、换一条、或者砌进墙里(merge)。


2. 顶层全景(它大概怎么转)

2.1 顶层结构图

怎么读这张图: 从上到下是一次 get_peft_model 调用的展开顺序;最底层是注入完成后单个目标层的前向结构。

LoraConfig(r=16, target_modules=["q_proj","v_proj"], ...)


① get_peft_model(model, config) mapping_func.py
│ 按 task_type 选 PeftModel 子类

② PeftModel ──持有──► LoraModel peft_model.py / lora/model.py
(外包装,task 接口) (BaseTuner 子类,注入引擎)
│ inject_adapter:遍历 named_modules 按名匹配

③ 每个命中的 nn.Linear 被 lora.Linear 原地替换 lora/layer.py
┌───────────────────────────────────────┐
│ y = W·x + (α/r)·B·A·dropout(x) │
│ ▲基座冻结 ▲ 低秩支路,可训练 │
└───────────────────────────────────────┘

2.2 部件职责

部件干什么在哪个文件
PeftType 枚举 + register_peft_method方法名册与注册函数,每个 tuner 包导入时自我登记src/peft/utils/peft_types.py:19:126
PEFT_TYPE_TO_*_MAPPING 四张表peft_type → config 类 / tuner 类 / 参数名前缀src/peft/mapping.py:30-33
get_peft_model统一入口:校验、分流、返回包装后的模型src/peft/mapping_func.py:105
PeftModel 及 task 子类外包装:forward 透传;prompt 类方法在这里拼接虚拟 tokensrc/peft/peft_model.py:119
BaseTuner注入引擎基类:遍历模块树、匹配、替换、冻结src/peft/tuners/tuners_utils.py:258
LoraModel / LoraLayerLoRA 的模型级逻辑与层级实现src/peft/tuners/lora/model.py:90src/peft/tuners/lora/layer.py:108
BaseTunerLayer所有「包住原层的适配层」的公共协议(merge/unmerge/set_adapter……)src/peft/tuners/tuners_utils.py:1808
save_and_load适配器权重的存与取,剥离/还原 adapter 名src/peft/utils/save_and_load.py:93:454

2.3 主线走一遍:从 get_peft_model 到第一次前向

下面五个阶段对应第 2、3 章的深入讲解:

阶段发生什么入口符号
① 分流config.peft_typetask_type,决定用哪个 PeftModel 子类包装get_peft_modelsrc/peft/mapping_func.py:188-203
② 建 tuner按注册表实例化 LoraModel,它包住原模型PeftModel.__init__src/peft/peft_model.py:166-176
③ 注入遍历 named_modules,名字匹配 target_modules 的层被适配层原地替换;其余参数全部 requires_grad=FalseBaseTuner.inject_adaptersrc/peft/tuners/tuners_utils.py:795
④ 前向每个被替换的层:先走冻结基座,再加上各活跃适配器的低秩支路输出Linear.forwardsrc/peft/tuners/lora/layer.py:1035
⑤ 存档只挑出适配器参数,删掉 key 里的 adapter 名,写 adapter_model.safetensors + adapter_config.jsonsave_pretrainedsrc/peft/peft_model.py:237

3. 阅读地图(建议顺序)

五章由浅入深。只想搞懂 LoRA 本身,读 01 → 03 足够;要自己加一个 PEFT 方法,读 02 → 04

顺序章节讲什么适合谁
101-lora-math.md低秩假设、ΔW=BA、α/r 缩放、初始化设计所有人必读,这是理解整个库的地基
202-registry-and-injection.md注册表、get_peft_model 分流、按名匹配注入的全流程想知道「一行调用背后发生了什么」的人
303-lora-layer-merge.mdLoraLayer 数据结构、forward、merge/unmerge、量化基座要动 LoRA 实现或排查训练/推理问题的人
404-other-methods.mdAdaLoRA、Prompt/P-/Prefix-tuning、IA³ 的核心机制选方法、写新 tuner 的人
505-training-save-load.mdkbit 训练准备、存档格式、加载与热插拔要把适配器训练落地到生产的人

4. 巧妙之处(可借鉴的技术)

每条先白话点题,再给锚点;细节在各章展开。

  • 注入靠「名字匹配 + 原地替换」,不碰模型源码。 不管模型是什么架构,只要模块名叫 q_proj 就能被替换(check_target_module_existssrc/peft/tuners/tuners_utils.py:2301)。这让 PEFT 对上游模型零侵入——这是它能覆盖 44 种方法 × 几乎所有 HF 模型的根本原因。见第 2 章。
  • adapter 名编进参数 key,多适配器因此几乎免费。 每个适配器的权重存在 nn.ModuleDict 里、key 带名字(lora_A["default"]),同一层可以挂任意多个;保存时再把名字剥掉(get_peft_model_state_dictsrc/peft/utils/save_and_load.py:106-109 docstring)。见第 3 章。
  • 可合并 = 推理零开销。 LoRA 的 delta 权重 B@A 形状和原权重一样,一行 weight += delta 就并回去了(get_delta_weightsrc/peft/tuners/lora/layer.py:1028)。部署时不用为适配器付任何延迟。见第 3 章。
  • target_modules="all-linear" shorthand + 目标列表自动压缩。 前者源自 QLoRA 仓库的做法(_maybe_include_all_linear_layerssrc/peft/tuners/tuners_utils.py:2390);后者在目标名超过 20 个时自动求最小匹配集,把「每个目标 × 每层」的匹配开销压下来(src/peft/tuners/tuners_utils.py:899-910)。见第 2 章。
  • 量化基座不挡路:dispatcher 链按基座类型选适配层实现。 bitsandbytes 8bit/4bit、GPTQ、AWQ、HQQ、torchao……按顺序逐个问「这个 target 你接吗」,第一个接的赢(_create_new_modulesrc/peft/tuners/lora/model.py:393-457)。见第 3 章。
  • safe_merge 先克隆检查 NaN 再落笔。 防止一个训坏的适配器把基座权重污染了才发现(Linear.mergesrc/peft/tuners/lora/layer.py:936-950)。
  • 二次注入有显式警告。 对已经注入过的模型再调 get_peft_model 会收到「先 .unload()」的提醒(src/peft/mapping_func.py:146-150),而不是悄悄叠两层。

5. 边界与局限

诚实清单——读源码和官方文档能确认的部分:

  • 「参数高效」是显存/存储意义上的,不保证质量打平全量微调。 某些任务上全量微调仍然更好;PEFT 的卖点是成本与可管理性(本库源卡片 Gotchas 亦有此结论)。
  • 适配器存档离开基座模型就是废物。 checkpoint 里没有基座权重,adapter_config.json 里的 base_model_name_or_path 指错了就加载出错模型;换基座等于换了参照系。get_peft_model 会对基座名变更发警告(src/peft/mapping_func.py:152-156)。
  • prompt 类方法占用上下文预算。 num_virtual_tokens 个虚拟 token 拼在输入前面(PeftModelForCausalLM.forwardsrc/peft/peft_model.py:2130-2132),长上下文场景里这是真实成本;且 prefix-tuning 与梯度检查点不兼容(_setup_prompt_encodersrc/peft/peft_model.py:707-709)。
  • 不是所有层都能合并。 lora_bias=True 但基座层没有 bias 时合并会直接报 RuntimeErrorsrc/peft/tuners/lora/layer.py:954-958);IA³ 的 unmerge 用除法近似还原,官方警告结果可能不准(src/peft/tuners/ia3/layer.py:146)。
  • 合并会改变基座 dtype 行为。 CPU 上对 bf16/fp16 权重做合并会先升 fp32 再算,因为部分 CPU 的半精度 matmul 很慢(get_delta_weightsrc/peft/tuners/lora/layer.py:1016-1031)。
  • 非 transformers 模型能用,但功能打折。 task 专属前向(如 prompt 拼接)依赖 transformers 约定;纯 nn.Module 只能用 inject_adapter_in_model 级别的能力(src/peft/mapping.py:47-96)。
  • 注入失败的信息密度高但要会读。 一个目标都没匹配上会抛 NoMatchingPeftModuleError,并按「全部被排除 / 全不匹配 / 混合」分三种文案(src/peft/tuners/tuners_utils.py:1032-1067)——写错 target_modules 是最常见的坑。

6. 横向对比

同书架(ai-frontier-reference)上围绕「模型训练」的兄弟库,各自站在不同层:

维度PEFT(本库)transformerstrlunslothaccelerate
站位改模型结构:加可训练旁路模型本体 + Trainer后训练算法(SFT/DPO/PPO)LoRA 训练的速度/显存极限优化分布式与混合精度启动层
与 LoRA 的关系LoRA 的实现方被 LoRA 包装的基座来源内部调 PEFT 挂 LoRA自写 kernel 实现 LoRA 前反向,兼容 PEFT 配置不管 LoRA,管进程与设备
典型调用get_peft_modelAutoModel.from_pretrainedSFTTrainer(peft_config=...)FastLanguageModel.get_peft_modelaccelerate launch
优化目标方法覆盖面 × 零侵入模型覆盖面算法正确性单卡吞吐与显存多卡正确启动

一句话:accelerate 管「几张卡」、transformers 管「什么模型」、PEFT 管「改哪几个参数」、trl 管「按什么算法训」、unsloth 管「怎么算得更快」——五者正交,实际项目里常常同时出现。

7. 代码地图(入口级)

每章末尾有更细的地图;这里只列从零读源码的五个入口:

主题文件路径符号名
统一入口与分流src/peft/mapping_func.pyget_peft_model
方法注册表src/peft/utils/peft_types.pysrc/peft/mapping.pyPeftTyperegister_peft_methodPEFT_TYPE_TO_CONFIG_MAPPING
注入引擎src/peft/tuners/tuners_utils.pyBaseTunerBaseTuner.inject_adaptercheck_target_module_exists
LoRA 层实现src/peft/tuners/lora/layer.pyLoraLayerLinear.forwardLinear.mergeLinear.get_delta_weight
存档与加载src/peft/utils/save_and_load.pyget_peft_model_state_dictset_peft_model_state_dictload_peft_weights