Chapter 02

安装与最小 LoRA 例子

📌 commit a106ff4c7061 10 行代码把 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):

  1. 根据 cfg.peft_type 找到对应的 tuner 类(LoraConfig → LoraModel);
  2. 遍历 model.named_modules(),对 name 命中 target_modules 的 nn.Linear,在外面包一层 LoraLayer
  3. requires_grad=False 应用到所有原始参数;
  4. 返回一个 PeftModel(PyTorch nn.Module 的子类)。

target_modules 是怎么匹配的

src/peft/tuners/tuners_utils.py::BaseTuner._check_target_module_exists

调试技巧:

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)
PeftModelForSequenceClassificationSEQ_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_dictutils/save_and_load.py
create_loraplus_optimizer optimizers/loraplus.py

"我要做 X,PEFT 有现成 API 吗?" → 先 grep src/peft/__init__.py,有就是公共 API,没有就要进 tuners/ 子目录找。

2.10这章你需要带走的