多 adapter 高级用法:加载 / 切换 / 合并 / 混合
PEFT 把"adapter"做成了一等公民:你可以在同一个 base 上挂任意多个 LoRA,运行时按需切换、按权重融合、甚至合成新的 adapter。 本章把这套机制讲透。
10.1多 adapter 在 PEFT 里的内部表示
回顾第 4 章:每个 LoRA layer 的 lora_A / lora_B 都是 nn.ModuleDict,按 adapter_name 索引:
self.lora_A: nn.ModuleDict = nn.ModuleDict({})
self.lora_B: nn.ModuleDict = nn.ModuleDict({})
self.scaling: dict = {}
self.active_adapter: Union[str, List[str]] = "default"
所以"加 adapter"= 给 ModuleDict 加一项;"切 adapter"= 改 active_adapter;"开关"= 改 disable_adapters 标志。
10.2加多个 adapter
from peft import LoraConfig, PeftModel, get_peft_model
# 1. 第一次:用 get_peft_model 注册第一个
cfg_a = LoraConfig(r=8, target_modules=["q_proj","v_proj"])
model = get_peft_model(base_model, cfg_a, adapter_name="task_a")
# 2. 加第二个
cfg_b = LoraConfig(r=16, target_modules=["q_proj","v_proj","k_proj"])
model.add_adapter(adapter_name="task_b", peft_config=cfg_b)
# 3. 加第三个(从磁盘加载已训好的)
model.load_adapter("./trained_lora_task_c", adapter_name="task_c")
print(model.peft_config) # 显示三个 adapter
print(model.active_adapter) # 'task_a'(add 后不自动切)
10.3切换 active adapter
model.set_adapter("task_b")
out_b = model.generate(input_ids)
model.set_adapter("task_a")
out_a = model.generate(input_ids)
# 同时激活多个 → forward 时各自 LoRA 输出相加
model.set_adapter(["task_a", "task_b"])
out_combined = model.generate(input_ids)
实现:PeftModel.set_adapter 会递归调每个 LoraLayer 的 set_adapter,把 active_adapter 改成对应字符串/列表。
10.4整体禁用 / 启用 adapter
# 临时禁用所有 adapter(推理时想看 base 输出)
with model.disable_adapter():
out_base = model.generate(input_ids)
# 等价的手动版本
model.disable_adapter_layers()
out_base = model.generate(input_ids)
model.enable_adapter_layers()
10.5多 adapter 权重合成
PEFT 提供 add_weighted_adapter 把多个 adapter 合成成一个新 adapter(不修改原始):
model.add_weighted_adapter(
adapters=["task_a", "task_b", "task_c"],
weights=[0.5, 0.3, 0.2],
adapter_name="merged_abc",
combination_type="linear", # ★ 合成方式
)
model.set_adapter("merged_abc")
combination_type 选项
| type | 含义 | 典型场景 |
|---|---|---|
linear | $\Delta W = \sum_i w_i B_i A_i$ | 简单加权融合 |
cat | $B = [B_1 | B_2]$, $A = [A_1; A_2]$(rank 累加) | 不损失任何信息 |
svd | 合成后做 truncated SVD 重新降秩 | 多 adapter 融合且保 r |
ties | TIES-Merging(去冗余 + 符号选举) | 多任务融合避免冲突 |
dare_linear | DARE(随机 drop + 线性融合) | 融合冲突任务 |
magnitude_prune | 按 magnitude 剪枝再融 | 稀疏融合 |
TIES / DARE 是2024 年流行的多任务融合算法,效果通常比 linear 好 1-2 pp。
10.6删除 adapter / 释放显存
model.delete_adapter("task_b") # 不再需要 task_b
# 此时 lora_A["task_b"] / lora_B["task_b"] 都被 del
10.7不同 batch 用不同 adapter(mixed-batch routing)
多任务推理时常见需求:同一个 batch 里不同 sample 想用不同 adapter。看 PeftModel.forward 支持的 adapter_names 参数:
# batch_size = 4,每个 sample 指定一个 adapter
adapter_names = ["task_a", "task_b", "task_a", "task_c"]
out = model(
input_ids=batch_ids,
attention_mask=batch_mask,
adapter_names=adapter_names, # ★ 关键
)
实现:BaseTunerLayer._mixed_batch_forward 会把 batch 按 adapter 切片,分别走对应 adapter,再合回输出。注意:
- 只有部分 tuner 支持(LoRA / IA3 / DoRA 支持,Prompt 系不支持);
- 会损失 batch 内并行(每个 adapter 单独跑一次小 batch);
- 所有指定的 adapter 必须先 add 进 model。
10.8多 adapter 推理服务化场景
(user_id=A)"] --> Router Req2["请求
(user_id=B)"] --> Router Router --> Model["共享 Base Model"] Model -.-> LoRA_A["LoRA A"] Model -.-> LoRA_B["LoRA B"] Model -.-> LoRA_C["LoRA C"] LoRA_A --> Out1[Response A] LoRA_B --> Out2[Response B]
vLLM、TGI、SGLang 等推理引擎都支持"多 LoRA 共享 base"的服务模式。一台 80GB GPU 装一个 base + N 个 LoRA(每个 ~10MB),N 可以上百。
10.9持久化:每个 adapter 各自一份目录
model.save_pretrained("./multi_adapter/") # 默认只保存 active
model.save_pretrained("./multi_adapter/", selected_adapters=["task_a","task_b"])
# 加载时
base = AutoModelForCausalLM.from_pretrained(MODEL)
model = PeftModel.from_pretrained(base, "./multi_adapter/", adapter_name="task_a")
model.load_adapter("./multi_adapter/task_b", adapter_name="task_b")
model.load_adapter("./multi_adapter/task_c", adapter_name="task_c")
10.10多 adapter 混合训练(不常用但能做)
用 PEFT 的 PeftMixedModel 可以在同一个 base 上同时挂 LoRA 和 IA³ 一起训:
from peft import PeftMixedModel, LoraConfig, IA3Config
lora_cfg = LoraConfig(target_modules=["q_proj","v_proj"])
ia3_cfg = IA3Config(target_modules=["k_proj","down_proj"],
feedforward_modules=["down_proj"])
model = PeftMixedModel(base, lora_cfg, adapter_name="lora")
model.add_adapter("ia3", ia3_cfg)
model.set_adapter(["lora", "ia3"]) # 两者同时激活,一起训
实际用得不多——LoRA 已经够好,混合反而调超参困难。
10.11常见错误
| 现象 | 处理 |
|---|---|
| add_adapter 后没生效 | 记得 set_adapter 切到新的 |
| add_weighted_adapter 报"adapter_names not in module" | 三个 adapter 的 target_modules 必须一致(不一致时 cat 模式可绕过) |
| mixed batch routing 失败 | 某些 sample 指定的 adapter 不存在;先把所有 adapter add 上 |
| save 后体积比预期大 | 默认保 active,其他 adapter 也想保用 selected_adapters |
| delete_adapter 后显存没降 | torch.cuda.empty_cache() 释放缓存 |
10.127 种 combination_type 全表
本章前面提到 add_weighted_adapter 支持多种融合策略。完整 7 种(实现位置 tuners/lora/model.py:652–900 + utils/merge_utils.py):
| combination_type | 实现 | 什么时候用 |
|---|---|---|
"linear" | 简单加权和(task_arithmetic, merge_utils.py:144) | 多 adapter 等权融合,最稳 |
"cat" | 沿 rank 维拼接 lora_A 和 lora_B(model.py:870) | 保留全部表达力;新 rank = sum(各 rank) |
"svd" | cat + SVD 降回 target_rank(model.py:880) | cat 太厚时压缩 |
"ties" | magnitude prune → 多数符号 mask → disjoint merge(merge_utils.py:185) | 多 adapter 冲突大时(论文 2306.01708) |
"dare_linear" | random prune (DARE) → 加权和(merge_utils.py:217) | 稀疏融合,减少冗余(论文 2311.03099) |
"dare_ties" | random prune + 多数符号(merge_utils.py:239) | DARE + TIES 组合,最稳的融合 |
"magnitude_prune" | 按 magnitude 剪后加权(merge_utils.py:163) | 只保留每个 adapter 最重要的更新 |
"该选哪个?"决策树:
- 2-3 个 adapter / 任务接近 →
"linear"(最简单); - 不损失表达力 →
"cat"(rank 翻倍但 0 信息损失); - cat 后 rank 太大 →
"svd"(再压回 target_rank); - 多 adapter 冲突明显(比如代码 + 数学 + 写作) →
"ties"或"dare_ties"。
10.13底层 merge 三件工具(utils/merge_utils.py)
TIES / DARE 不是单独算法,而是 3 个工具的组合:
| 函数 | 位置 | 作用 |
|---|---|---|
magnitude_based_pruning(tensor, density) | line 37–53 | 保留 top-k abs 值,其余清零 |
random_pruning(tensor, density, rescale) | line 56–72 | Bernoulli mask + 期望保持的 rescale(DARE 用) |
calculate_majority_sign_mask(task_tensors, method) | line 103–125 | 多 task 的"多数符号"投票("total" 或 "frequency") |
disjoint_merge(task_tensors, mask) | line 128–141 | 按 mask 求和并归一 |
task_arithmetic(task_tensors, weights) | line 144–160 | 纯加权和(linear baseline) |
组合关系:
- TIES =
magnitude_based_pruning+calculate_majority_sign_mask+disjoint_merge - DARE-Linear =
random_pruning(rescale=True)+task_arithmetic - DARE-TIES =
random_pruning(rescale=True)+calculate_majority_sign_mask+disjoint_merge
这 3 个工具是"模型合并"领域 model soup / model fusion 的标准砖块,不只用在 PEFT。LLM 模型融合(mergekit 之类工具)的逻辑大同小异。
10.14LoraPlus 优化器:分组学习率
训 LoRA 时 lora_A(基底)和 lora_B(任务向量)的最优学习率差一个数量级——B 应该大、A 应该小。src/peft/optimizers/loraplus.py 的 create_loraplus_optimizer 自动分组:
| 参数组 | 学习率 | 说明 |
|---|---|---|
| lora_A 矩阵 | base lr | 低 |
| lora_B 矩阵 | base lr × loraplus_lr_ratio(默认 2-16) | 高(任务方向) |
| embedding LoRA | loraplus_lr_embedding(默认 1e-6) | 极低(embedding 容易过拟合) |
| weight_decay | 只作用于非 bias / 非 norm 参数 | — |
用法:
from peft.optimizers import create_loraplus_optimizer
optimizer = create_loraplus_optimizer(
model=peft_model,
optimizer_cls=torch.optim.AdamW,
lr=5e-5,
loraplus_lr_ratio=8.0, # B 矩阵 LR = lr × 8 = 4e-4
loraplus_lr_embedding=1e-6,
weight_decay=0.01,
)
实测在 7B-32B 模型上 LoraPlus 比 AdamW 单一 LR 收敛快 1.5-3×,最终精度也高一点。没成本就能用,强烈建议。
10.15这章你需要带走的
- 所有 LoRA 参数都按
adapter_name索引存 ModuleDict,多 adapter 是一等公民; - 三个核心动词:
add_adapter / set_adapter / delete_adapter; add_weighted_adapter支持 linear / cat / svd / ties / dare 等 6 种融合策略;- 多 adapter 推理服务化用 vLLM / TGI / SGLang 都已经支持;
- 同 batch 不同 adapter 通过
forward(adapter_names=[...])实现; - 不同算法(LoRA + IA³)混挂用
PeftMixedModel。