Chapter 10

多 adapter 高级用法:加载 / 切换 / 合并 / 混合

📌 commit a106ff4c7061 一个 base 挂多个 LoRA 的全套操作

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_linearDARE(随机 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,再合回输出。注意

10.8多 adapter 推理服务化场景

flowchart LR Req["请求
(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 最重要的更新

"该选哪个?"决策树:

  1. 2-3 个 adapter / 任务接近"linear"(最简单);
  2. 不损失表达力"cat"(rank 翻倍但 0 信息损失);
  3. cat 后 rank 太大"svd"(再压回 target_rank);
  4. 多 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)

组合关系:

这 3 个工具是"模型合并"领域 model soup / model fusion 的标准砖块,不只用在 PEFT。LLM 模型融合(mergekit 之类工具)的逻辑大同小异。

10.14LoraPlus 优化器:分组学习率

训 LoRA 时 lora_A(基底)和 lora_B(任务向量)的最优学习率差一个数量级——B 应该大、A 应该小。src/peft/optimizers/loraplus.pycreate_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这章你需要带走的