跳到主要内容

数据截至 (上游 commit 5779b17b9a67)

04 · 方法动物园:AdaLoRA、Prompt 系与 IA³

这一章讲什么: LoRA 之外,挑四个代表性方法讲透:AdaLoRA(权重增量族里最精巧的)、Prompt-tuning 与 P-tuning 与 Prefix-tuning(输入族三兄弟)、IA³(激活族代表)。读法和前几章一样:先讲机制,再对到真实代码。


1. 鸟瞰:按「参数住在哪」分三族

可训练参数代表方法基座权重动吗
权重增量族每层一个低秩/稀疏的 ΔWLoRA、AdaLoRA、DoRA、VeRA、LoHa、BoFT……训练时不动,可合并
输入族一段虚拟 token 的嵌入(或逐层 KV 前缀)Prompt-tuning、P-tuning、Prefix-tuning完全不动
激活族每层一个逐通道缩放向量IA³不动;合并 = 逐通道乘

三族共用第 2 章的注册表/注入骨架。差别只在于:输入族没有「注入」这一步——它们不改模块树,而是靠 PeftModel.forward 在输入上做手脚,所以注册时 model_cls 是 prompt encoder 而不是 tuner(register_peft_method 的注释明说这个特例,src/peft/utils/peft_types.py:177)。


2. AdaLoRA:把「秩」也变成被优化的对象

2.1 它要解决的小问题

LoRA 的 r 是全局统一的死超参。但直觉上,不同层需要的容量不同——有的层 ΔW 信息量大,有的几乎是恒等。AdaLoRA(arXiv:2303.10512)让训练过程自己决定每层留多少秩,在总预算不变的前提下把秩分给最需要的层。

2.2 两个机制

机制一:SVD 式参数化。 普通 LoRA 是 ΔW = B·A;AdaLoRA 改成 ΔW = B·diag(E)·A——中间夹一个奇异值向量 E,三个因子分别扮演左奇异向量、奇异值、右奇异向量。建层代码里三者是独立的 nn.Parametersrc/peft/tuners/adalora/layer.py:69-73):

self.lora_A[adapter_name] = nn.Parameter(torch.randn(r, self.in_features))
self.lora_E[adapter_name] = nn.Parameter(torch.randn(r, 1)) # 奇异值
self.lora_B[adapter_name] = nn.Parameter(torch.randn(self.out_features, r))

机制二:秩预算调度。 RankAllocatorsrc/peft/tuners/adalora/layer.py:350)维护一条三次方衰减的预算曲线:从初始总秩(所有层 r 之和)逐步压到 target_r × 层数budget_schedulesrc/peft/tuners/adalora/layer.py:390-407)。每隔 deltaT 步,按「重要性评分」剪掉最不重要的一批奇异值——评分是灵敏度(|参数×梯度| 的滑动平均)乘以不确定性(update_iptsrc/peft/tuners/adalora/layer.py:409-430)。

调度本身不在训练循环里自动发生:训练代码要显式调 model.base_model.update_and_allocate(global_step)src/peft/tuners/adalora/model.py:328),它内部转调 rankallocator.update_and_allocatesrc/peft/tuners/adalora/model.py:352)。AdaLoRA 是少数要求训练循环配合的方法——这是它和「换 config 即用」的大多数方法最大的使用差异。


3. 输入族三兄弟:虚拟 token

三者的共同点:基座模型一参数不动,可训练的是一小段「学出来的输入」。差别在虚拟信息注入到哪一层、过不过编码器。

3.1 Prompt-tuning:最简形态

可训练参数只有一个 nn.Embedding(num_virtual_tokens, token_dim)PromptEmbeddingsrc/peft/tuners/prompt_tuning/model.py:63-67)。前向就是查表(src/peft/tuners/prompt_tuning/model.py:103-106)。

初始化有两种讲究(src/peft/tuners/prompt_tuning/model.py:68-101):

  • SAMPLE_VOCAB:从词表随机采样 token,用它们的词向量初始化;
  • TEXT:给一句自然语言(如「判断这条评论的情感」),tokenize 后用词向量初始化——把 prompt 工程变成初始化工程。文本比虚拟 token 长就截断,短就循环重复(src/peft/tuners/prompt_tuning/model.py:90-96)。

3.2 P-tuning:给虚拟 token 加一个编码器

P-tuning 的观察是:虚拟 token 之间不该相互独立。PromptEncodersrc/peft/tuners/p_tuning/model.py:26)在 embedding 之后加一个重参数化头——MLP(两层 Linear+ReLU)或双向 LSTM+MLP(src/peft/tuners/p_tuning/model.py:84-121),让虚拟 token 的表示互相「看见」彼此。编码器只在训练模式构建(if not config.inference_mode: 守卫,src/peft/tuners/p_tuning/model.py:84)——推理存档只留算出来的嵌入即可。

3.3 Prefix-tuning:不进输入,进每一层的 KV

Prefix-tuning 更深一层:学的不是输入嵌入,而是每层注意力的 key/value 前缀PrefixEncodersrc/peft/tuners/prefix_tuning/model.py:22)默认用一个 MLP 把 num_virtual_tokens 个位置编码投影成 num_layers * 2 * token_dim 维(src/peft/tuners/prefix_tuning/model.py:67-73);get_promptsrc/peft/peft_model.py:763)再把它 reshape 成逐层的 past_key_values 喂给基座(reshape/permute 在 src/peft/peft_model.py:785-798)。

3.4 输入族的前向:在 PeftModel 里拼接

因为模块树没被改动,三兄弟的效果全部发生在 PeftModelForCausalLM.forwardsrc/peft/peft_model.py:2047)的 prompt 分支里:

prompt 分支(非 prefix):
inputs_embeds = word_embeddings(input_ids)
prompts = get_prompt(batch_size) # 查表/过编码器
inputs_embeds = cat([prompts, inputs_embeds]) # 拼在输入前面
labels = cat([全 -100 前缀, labels]) # 虚拟 token 不算 loss
prefix 分支:
kwargs["past_key_values"] = get_prompt(...) # 作为 KV 前缀注入

(对应 src/peft/peft_model.py:2124-2133:2112-2120;attention mask 前面补 num_virtual_tokens 个 1 在 :2094-2097。)labels 补 -100 是 PyTorch loss 的「忽略此位置」约定——虚拟 token 参与前向但不计 loss。

这些编码器的挂在哪、词表怎么找,由 _setup_prompt_encoder 完成(src/peft/peft_model.py:644):先按常见名字找词嵌入层,找不到就遍历参数、谁的第 0 维等于 vocab_size 就认定谁是词嵌入(src/peft/peft_model.py:666-700)——一个针对任意 HF 模型的启发式。


4. IA³:每层只学一个向量

IA³(arXiv:2205.05638)把「高效」推到极限:不增加秩,只对已有激活做逐通道缩放。每层只学一个向量 l

  • 注意力/输出侧:y = (Wx) ⊙ l——缩放输出通道;
  • 前馈层(feedforward_modules):y = W(x ⊙ l)——缩放输入维度。

向量初始化成全 1(恒等缩放),参数量是每层 out_featuresin_features 个数——比 LoRA 再少两三个数量级。建层代码按 is_feedforward 决定向量形状(src/peft/tuners/ia3/layer.py:62-66),初始化在 reset_ia3_parameterssrc/peft/tuners/ia3/layer.py:72-75)。

前向的真实实现(Linear.forwardsrc/peft/tuners/ia3/layer.py:162-188)和多适配器合成方式值得一看:多个活跃适配器的缩放向量逐元素相乘ia3_scaling *= ...src/peft/tuners/ia3/layer.py:171-175)——缩放族天然满足乘法交换律,所以合成是乘而不是加。

合并也因此不同:merge 是把基座权重lsrc/peft/tuners/ia3/layer.py:117-129),unmerge 只能回去——除法加 1e-8 防零,并且官方警告「Unmerge result can be inaccurate for (IA)³」(src/peft/tuners/ia3/layer.py:146-154)。这是「乘法型适配器」相对「加法型 LoRA」的固有代价。


5. 关键细节与坑

  • AdaLoRA 忘了调 update_and_allocate = 退化成固定秩。 调度由训练循环驱动,框架不替你做;AdaLoraModel.forward 透传基座前向,不含调度逻辑(src/peft/tuners/adalora/model.py:229)。
  • AdaLoRA 允许 r=0 表示该层被剪到零秩(src/peft/tuners/adalora/layer.py:55-57),普通 LoRA 会直接报错。
  • Prefix-tuning 与梯度检查点不兼容。 因为 prefix 走 Cache 路径,_setup_prompt_encoder 直接抛错(src/peft/peft_model.py:709)。
  • 输入族方法不支持多适配器。 存档 key 里不带 adapter 名——get_peft_model_state_dict 的注释明说了这一点(src/peft/utils/save_and_load.py:135-143)。想要「一个基座多个任务」请用 LoRA 系。
  • 虚拟 token 占用真实上下文长度。 拼接后序列变长 num_virtual_tokens,注意力成本和最大可用长度都受影响;prefix 分支还会把 max_cache_len 相应加上(src/peft/peft_model.py:2115-2118)。
  • P-tuning 的 LSTM 头在 transformers 生态里几乎只用 MLP。 代码里 MLP 分支对 encoder_num_layers 发「此参数被忽略」的警告(src/peft/tuners/p_tuning/model.py:104-110)——MLP 的层数被固定为默认结构。
  • 多任务版 prompt tuning 存在但独立。 MultitaskPromptEmbedding 继承 PromptEmbeddingsrc/peft/tuners/multitask_prompt_tuning/model.py:28),是另一篇论文的机制,不在本章展开。

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

主题文件路径符号名
AdaLoRA 层与秩分配src/peft/tuners/adalora/layer.pyAdaLoraLayerSVDLinearRankAllocator
AdaLoRA 模型与调度入口src/peft/tuners/adalora/model.pyAdaLoraModel.update_and_allocate
Prompt-tuningsrc/peft/tuners/prompt_tuning/model.pyPromptEmbedding
P-tuningsrc/peft/tuners/p_tuning/model.pyPromptEncoder
Prefix-tuningsrc/peft/tuners/prefix_tuning/model.pyPrefixEncoder
输入族前向拼接src/peft/peft_model.pyPeftModelForCausalLM.forwardPeftModel.get_prompt_setup_prompt_encoder
IA³src/peft/tuners/ia3/layer.pysrc/peft/tuners/ia3/model.pyIA3LayerLinearIA3Model
方法注册入口src/peft/utils/peft_types.pyregister_peft_method