Prompt / Prefix / P-Tuning:不改权重的另一条路
LoRA 系是"改 Linear 权重"的思路;本章的方法走另一条路—— 权重一行不动,在输入或每层注入可学的"软 prompt"。 这种路径参数量更小、保存更轻,但训练通常更难调。
9.1三种方法的关系
| 方法 | 注入位置 | 注入形式 | 论文 |
|---|---|---|---|
| Prompt Tuning | 仅 embedding 层 | $n$ 个可学 token embedding,prepend | Lester et al. 2021 |
| Prefix Tuning | 每个 transformer 层 | 注入到 K 和 V 的 cache | Li & Liang 2021 |
| P-Tuning v1 | 仅 embedding 层 | 同 Prompt Tuning,但中间过 MLP 重参数化 | Liu et al. 2021 |
| P-Tuning v2 | 每个 transformer 层 | 同 Prefix,但去掉重参数化 MLP | Liu et al. 2022 |
| Multitask Prompt | 仅 embedding 层 | 跨任务共享的 prompt + 任务特定 prompt | Wang et al. 2023 |
9.2Prompt Tuning(最简单)
设可学 prompt $P = [p_1, p_2, \ldots, p_n] \in \mathbb{R}^{n \times d}$,把它prepend 到输入 embedding 前:
$$H_0 = [P \;;\; \text{Embed}(x_1), \ldots, \text{Embed}(x_T)] \in \mathbb{R}^{(n+T)\times d}$$
训练时只更新 $P$ 这 $nd$ 个参数。LLaMA-7B 用 $n=20$,总可训参数仅 80K。
用法
from peft import PromptTuningConfig, PromptTuningInit, get_peft_model, TaskType
cfg = PromptTuningConfig(
task_type=TaskType.CAUSAL_LM,
num_virtual_tokens=20,
prompt_tuning_init=PromptTuningInit.TEXT, # 用真实文本初始化
prompt_tuning_init_text="Translate the input English text to French:",
tokenizer_name_or_path="meta-llama/Llama-2-7b-hf",
)
model = get_peft_model(model, cfg)
实现:src/peft/tuners/prompt_tuning/model.py。PromptTuningInit 取值:
RANDOM:高斯初始化(不稳);TEXT:用真实文本 tokenize 后的 embedding 初始化(推荐);SAMPLE_VOCAB:从词表随机采 n 个 token。
9.3Prefix Tuning
Prompt Tuning 只在 embedding 层注入,信息要"流"过整个 transformer 才能到达深层。Prefix Tuning 直接在每一层都注入——具体是给 KV cache 加上可学的"虚拟 key/value":
$$K_l^{new} = [P^K_l \;;\; K_l],\quad V_l^{new} = [P^V_l \;;\; V_l],\;\; l = 1\ldots L$$
每层 prefix 长度 $n$、维度 $d$,所以总参数 $2Lnd$。$L = 32$、$d = 4096$、$n = 20$ 时约 5.2M——比 Prompt Tuning 大但仍然小。
from peft import PrefixTuningConfig, get_peft_model
cfg = PrefixTuningConfig(
task_type=TaskType.CAUSAL_LM,
num_virtual_tokens=20,
prefix_projection=True, # ★ 中间过一层 MLP 重参数化(稳定训练)
encoder_hidden_size=512, # MLP 隐层
)
model = get_peft_model(model, cfg)
实现:src/peft/tuners/prefix_tuning/。注意 prefix_projection=True 是必要的——直接学 $P^K, P^V$ 不稳,要经过一个 MLP $\phi$ 再映射出来:
$$P^K_l, P^V_l = \phi_l(\text{Embed}(\text{ids}))$$
9.4P-Tuning v1 / v2
| 版本 | 关键差异 | 对应 PEFT |
|---|---|---|
| P-Tuning v1 | Prompt Tuning + 中间用 LSTM/MLP 重参数化 | PromptEncoderConfig |
| P-Tuning v2 | Prefix Tuning + 去掉 MLP / 应用 BERT 类任务 | PrefixTuningConfig(prefix_projection=False) |
P-Tuning v1 用法
from peft import PromptEncoderConfig, PromptEncoderReparameterizationType, get_peft_model
cfg = PromptEncoderConfig(
task_type=TaskType.SEQ_CLS,
num_virtual_tokens=20,
encoder_reparameterization_type=PromptEncoderReparameterizationType.MLP,
encoder_hidden_size=128,
encoder_num_layers=2,
)
实现:src/peft/tuners/p_tuning/model.py。
9.5训练时的特殊处理
这一系列方法在 forward 时需要"把 prefix 拼到 input",PEFT 通过 PeftModelForXXX 子类的 forward 实现。看 src/peft/peft_model.py::PeftModelForCausalLM.forward 的简化逻辑:
def forward(self, input_ids, attention_mask, ...):
peft_type = self.peft_config[self.active_adapter].peft_type
if peft_type == PeftType.PROMPT_TUNING:
# 1) 生成 prompt embedding
prompts = self.get_prompt(batch_size)
# 2) 拼到 inputs_embeds 前
inputs_embeds = self.word_embeddings(input_ids)
inputs_embeds = torch.cat([prompts, inputs_embeds], dim=1)
# 3) attention_mask 也要扩长
attention_mask = torch.cat([torch.ones(b, n).to(...), attention_mask], dim=1)
return self.base_model(inputs_embeds=inputs_embeds, attention_mask=attention_mask, ...)
elif peft_type == PeftType.PREFIX_TUNING:
# 1) 生成每层 past_key_values
past_key_values = self.get_prompt(batch_size)
return self.base_model(input_ids=input_ids, past_key_values=past_key_values, ...)
所以这些方法必须用 PeftModelForXXX.forward,不能像 LoRA 那样直接 model.base_model.forward。
9.6三种方法的优劣对照
| 方法 | 参数量(7B) | 效果 | 训练稳定性 | 推理开销 |
|---|---|---|---|---|
| Prompt Tuning | ~80K | 较低(small) | 低 | 低 |
| Prefix Tuning | ~5M | 中 | 中 | 中(KV 变长) |
| P-Tuning v1 | ~200K + MLP | 较 PT 好 | 较好 | 低 |
| P-Tuning v2 | ~5M | 接近全参 FT | 较好 | 中 |
| LoRA r=8 | ~3M | 高 | 高 | 低(可 merge) |
结论:新项目几乎都选 LoRA,因为它训练稳定、效果好、可 merge。Prompt 系适合:
- 研究:复现论文;
- 极端多任务:要存上千个 adapter;
- BERT 系小模型分类任务。
9.7实战:用 Prompt Tuning 做"客户专属语气"
场景:base 模型回答正常,想给客户 A 专属语气(如总是"亲,您好~"开头)。
cfg = PromptTuningConfig(
task_type=TaskType.CAUSAL_LM,
num_virtual_tokens=10,
prompt_tuning_init=PromptTuningInit.TEXT,
prompt_tuning_init_text="Reply in friendly tone like a customer service rep:",
tokenizer_name_or_path=MODEL,
)
model = get_peft_model(model, cfg)
# 每个客户的 prompt 训完只需 ~80 KB 存储
model.save_pretrained(f"./prompts/customer_a")
同一台 GPU 可以同时挂上千个客户的 prompt——这是 Prompt Tuning 真正发力的场景。
9.8常见踩坑
| 现象 | 原因 | 处理 |
|---|---|---|
| Prompt Tuning loss 不降 | 用了 RANDOM 初始化 | 换 TEXT 初始化 |
| Prefix Tuning 训中崩 | 没开 prefix_projection | prefix_projection=True |
| 推理时输出多了奇怪开头 | generate 时 prompt 没被自动去除 | 用 PeftModel.generate 而非 base_model.generate |
| attention_mask 长度对不上 | 自己处理 mask 时忘了加 virtual tokens 的部分 | 用 model.prepare_inputs_for_generation 自动处理 |
| num_virtual_tokens=100 反而变差 | 过多 virtual token 抢占了真实输入的注意力预算 | 典型 sweet spot 是 10-30 |
9.9四种 Prompt 系实现的源码锚
四个 prompt 系算法在 PEFT 里都是独立 tuner,差异主要在"虚拟 token 怎么变成 prompt embedding"这一步:
| tuner | 位置 | 核心类 / 字段 |
|---|---|---|
PromptTuning | tuners/prompt_tuning/ | PromptTuningConfig (config.py:30);PromptEmbedding(直接 nn.Embedding,无 encoder) |
PrefixTuning | tuners/prefix_tuning/ | PrefixTuningConfig (config.py:23);PrefixEncoder(可选 MLP encoder) |
PTuning | tuners/p_tuning/ | PromptEncoderConfig (config.py:29);PromptEncoder(MLP 或 LSTM) |
MultitaskPromptTuning | tuners/multitask_prompt_tuning/ | 继承 PromptTuningConfig + 多任务 init 字段(见 ch08.10) |
9.10三者的真实差异(按"动哪里")
课本上说三者都是"prompt-based",但在"prompt embedding 作用于模型哪个位置"这一点上很不同:
| 方法 | 作用位置 | 参数量 | encoder 重参数化 |
|---|---|---|---|
| Prompt Tuning | 仅在 input embedding 前面 prepend N 个虚拟 token | N × hidden_size | 无(直接 Embedding) |
| Prefix Tuning | 在每一层的 K/V 前面注入 N 个 prefix | N × num_layers × 2 × hidden_size | 可选 MLP(prefix_projection=True) |
| P-Tuning (v1) | 仅在 input embedding,但过一个 encoder | N × hidden_size + encoder 参数 | 必须有(MLP / LSTM) |
| P-Tuning v2 | ≈ Prefix Tuning(生产代码统一用 Prefix Tuning 表达) | 同 Prefix Tuning | — |
9.11PromptEncoderReparameterizationType
P-Tuning 的 encoder_reparameterization_type(config.py:41–56)有两种取值:
| 类型 | 实现 | 用 |
|---|---|---|
"MLP" | 2 层 MLP(默认 encoder_num_layers=2) | 主流;快 |
"LSTM" | 双向 LSTM | 原始论文用;现在很少用,慢且效果接近 MLP |
PromptEncoder 的字段:encoder_hidden_size、encoder_num_layers、encoder_dropout。
9.12这章你需要带走的
- Prompt / Prefix / P-Tuning 是不改权重的另一条路,只在输入端塞可学 token;
- Prompt Tuning 只动 embedding 层;Prefix Tuning 注入每一层的 KV cache;
- P-Tuning v1 = Prompt Tuning + MLP 重参数化;v2 ≈ Prefix Tuning;
- Prompt Tuning 的 TEXT 初始化是必须的;Prefix Tuning 必须开 prefix_projection;
- 这些方法用
PeftModelForXXX.forward,不能绕过; - 大多数场景仍然 LoRA 更优,Prompt 系真正发力在"多 adapter 共存"。