安装与最小 LoRA 例子
PEFT 没有自家的 CUDA 扩展,全靠 PyTorch + transformers。装起来非常简单, 本章给最小可跑的 LoRA 训练脚本,并讲清楚每一行在做什么。
2.1安装
三条路径,按场景选:
| 场景 | 命令 | 说明 |
|---|---|---|
| 稳定版 | pip install peft | 大多数情况首选 |
| 跟最新 | pip install git+https://github.com/huggingface/peft | 装 main 分支 |
| 开发/读源码 | git clone … && pip install -e . | 本仓 commit a106ff4c7061 |
依赖矩阵(看 pyproject.toml)
| 包 | 版本约束 | 角色 |
|---|---|---|
| torch | >= 1.13 | 底座,建议 ≥ 2.1 |
| transformers | >= 4.41 | 模型源 |
| accelerate | >= 0.21 | 多卡/低显存 |
| safetensors | >= 0.4 | adapter 落盘 |
| huggingface_hub | >= 0.25 | push/pull adapter |
| bitsandbytes | >= 0.43 | QLoRA 才需要 |
| peft 本身 | 0.18.0+ | 当前 main 已含 40+ tuner |
验证装好了
python -c "import peft; print(peft.__version__); from peft import LoraConfig; print('ok')"
2.2最小 10 行 LoRA
下面是能在单卡跑起来的最小可复现脚本(约 10 行核心代码)。先看代码、再逐行讲解:
from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
from datasets import load_dataset
# 1. 加载 base 模型
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B", torch_dtype="bfloat16")
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B")
# 2. 配 LoRA
peft_cfg = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=8, lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
)
model = get_peft_model(model, peft_cfg)
model.print_trainable_parameters() # → trainable: ~0.1%
# 3. 数据(极简:把 wikitext 当原始语料)
ds = load_dataset("wikitext", "wikitext-2-raw-v1", split="train[:500]")
def tk(x): return tok(x["text"], truncation=True, max_length=512)
ds = ds.map(tk, remove_columns=ds.column_names)
# 4. 训
args = TrainingArguments(output_dir="./lora_out", per_device_train_batch_size=2,
num_train_epochs=1, save_strategy="epoch", bf16=True)
Trainer(model=model, args=args, train_dataset=ds,
data_collator=lambda d: tok.pad(d, return_tensors="pt")).train()
# 5. 保存
model.save_pretrained("./lora_out/final")
跑完 ./lora_out/final/ 下产物:
adapter_config.json # LoRA 配置
adapter_model.safetensors # 只有 LoRA 部分,约 5 MB
README.md
注意 base 模型权重没有被复制保存,只保存了 5MB 的 adapter——这就是 PEFT 的核心价值。
2.3逐行解读
get_peft_model(model, cfg)
这是 PEFT 唯一的"主入口"。它做四件事(看 src/peft/mapping_func.py):
- 根据
cfg.peft_type找到对应的 tuner 类(LoraConfig → LoraModel); - 遍历
model.named_modules(),对 name 命中target_modules的 nn.Linear,在外面包一层LoraLayer; - 把
requires_grad=False应用到所有原始参数; - 返回一个
PeftModel(PyTorch nn.Module 的子类)。
target_modules 是怎么匹配的
看 src/peft/tuners/tuners_utils.py::BaseTuner._check_target_module_exists:
- 如果
target_modules是 list:用 endswith 匹配(如"q_proj"匹配model.layers.0.self_attn.q_proj); - 如果是 str:当成 regex 用
re.fullmatch; - 如果是
"all-linear":匹配所有nn.Linear(除了lm_head)。
调试技巧:
for name, m in model.named_modules():
if isinstance(m, torch.nn.Linear):
print(name)
# 看输出再决定 target_modules
LoRA 公式回顾
每个被替换的 Linear 从原版 $y = Wx$ 变成:
$$y = W x + \frac{\alpha}{r}\, BA\, x$$
$W \in \mathbb{R}^{d\times d}$ 冻结,$B \in \mathbb{R}^{d\times r}$ 初始化为 0,$A \in \mathbb{R}^{r\times d}$ 高斯初始化。可学参数量 = $2dr$,远小于 $d^2$。
2.4加载 adapter 推理
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B", torch_dtype="bfloat16")
model = PeftModel.from_pretrained(base, "./lora_out/final")
model.eval()
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B")
out = model.generate(**tok("Once upon a time", return_tensors="pt").to(model.device),
max_new_tokens=30)
print(tok.decode(out[0]))
加载只需要一行 PeftModel.from_pretrained:它会自动从 adapter_config.json 读 LoRA 配置 + 把 adapter_model.safetensors 挂到 base 上。
2.5合并 LoRA 回 base(merge_and_unload)
训练时为了反向需要 LoRA 分开;推理时把它合回 base 一次省去 LoRA forward,速度快 20%:
merged = model.merge_and_unload() # 返回 base 模型(已加进 BA)
merged.save_pretrained("./merged_model")
# 接下来用 transformers 直接加载,无需 PEFT 依赖
实现见 src/peft/tuners/lora/layer.py::LoraLayer.merge,原理就是 $W' = W + \frac{\alpha}{r}BA$。
2.6常见问题
| 问题 | 处理 |
|---|---|
ValueError: Target modules q_proj not found |
模型层名不一样,先 print 一遍 named_modules |
| trainable% = 0 | print_trainable_parameters 看是否所有 LoRA 都没匹配上 |
| loss 一直不降 | lora_alpha 与 r 比例失衡;常见 alpha = 2r 起步 |
| save 后加载 missing keys | 用 PeftModel.from_pretrained 而非 load_state_dict |
| merge 后效果变差 | quantized 模型不能直接 merge,需要先 dequantize |
2.7PeftModelForXxx:6 个任务子类
2.x 的最简代码用的是基类 PeftModel,实际生产代码都用任务特化子类。它们都在 src/peft/peft_model.py,每个针对 HF 的一类 head:
| 子类 | task_type | 负责什么 |
|---|---|---|
PeftModelForCausalLM | CAUSAL_LM | 包 forward + generate,含 CrossEntropyLoss(line 1711–1826) |
PeftModelForSeq2SeqLM | SEQ_2_SEQ_LM | encoder-decoder,含可选 encoder-only LoRA dispatch(line 1827–2015) |
PeftModelForSequenceClassification | SEQ_CLS | 自动把 classifier 加入 modules_to_save(line 1761–1765) |
PeftModelForTokenClassification | TOKEN_CLS | NER / POS |
PeftModelForFeatureExtraction | FEATURE_EXTRACTION | embedding 模型 |
PeftModelForQuestionAnswering | QUESTION_ANS | QA span head |
实战写法:
from peft import LoraConfig, TaskType, get_peft_model
cfg = LoraConfig(
r=16,
task_type=TaskType.CAUSAL_LM, # ★ 这一行决定挑哪个 PeftModelForXxx
target_modules="all-linear",
)
peft_model = get_peft_model(base, cfg) # 返回的是 PeftModelForCausalLM
不写 task_type 也能跑(默认 None 走基类 PeftModel),但分类任务必须写——否则 classifier.weight 不会被保存到 adapter,加载回来是冷启动。
2.8AutoPeftModelForXxx:自动加载
不知道 base 模型的人加载 adapter 时最方便。src/peft/auto.py 提供 AutoPeftModelForXxx 类,跟 transformers.AutoModelForXxx 一一对应:
from peft import AutoPeftModelForCausalLM
# 自动:读 adapter_config.json → 找 base_model_name_or_path
# → 加载 base → 套 adapter
model = AutoPeftModelForCausalLM.from_pretrained("saves/qwen3-4b/lora/sft")
三个 Auto 类的差异:
| Auto 类 | 路由 |
|---|---|
AutoPeftModel | 不知道 task type,按 base 自动猜(基本是 CausalLM) |
AutoPeftModelForCausalLM | 强制 CAUSAL_LM |
AutoPeftModelForSeq2SeqLM | 强制 SEQ_2_SEQ_LM |
AutoPeftModelForSequenceClassification | 分类 |
| ... | 同样有 token / QA / feature 各一 |
"为什么不直接用 AutoPeftModel?" → 因为它会去猜,分类 head 这种 base 模型本身没有的,要靠 task_type 明确告诉。生产代码强烈建议用具体的 AutoPeftModelForXxx。
2.9公共 API 边界(src/peft/__init__.py)
第一次读 PEFT 代码的入口就是 __init__.py。重要导出(不到 100 行):
| 公共符号 | 位置 |
|---|---|
get_peft_model / PeftModel | peft_model.py |
LoraConfig / AdaLoraConfig / IA3Config / ... 30+ 个 | tuners/<name>/config.py |
PeftType / TaskType enum | utils/peft_types.py |
AutoPeftModelForXxx | auto.py |
PeftMixedModel | mixed_model.py |
prepare_model_for_kbit_training | utils/other.py,给 QLoRA 做 cast / requires_grad 处理 |
get_peft_model_state_dict / set_peft_model_state_dict | utils/save_and_load.py |
create_loraplus_optimizer | optimizers/loraplus.py |
"我要做 X,PEFT 有现成 API 吗?" → 先 grep src/peft/__init__.py,有就是公共 API,没有就要进 tuners/ 子目录找。
2.10这章你需要带走的
- 装 PEFT =
pip install peft,没有 C++ 扩展,问题最少; - 核心 API 就两个:
get_peft_model(套上 LoRA) +PeftModel.from_pretrained(加载); target_modules用 endswith 匹配,"all-linear"是简便选项;- LoRA 公式:$y = Wx + (\alpha/r)BAx$,B 零初始化保证训练前等价于 base;
- 推理性能关键操作:
merge_and_unload,把 LoRA 焊回 base 模型。