微调一个新任务就忘掉旧能力?模型合并 TIES/DARE 生产级实战
不用再训第三次 SFT:把多个微调 checkpoint 在权重空间里算术相加,用一顿午饭的钱换一个多能力模型。
开篇:从一个真实业务场景说起
假设你在做一个企业客服 Agent。第一轮迭代,你拿 Qwen3-4B 做 LoRA 微调,教会它按你们的工具调用格式(tool call JSON)输出,工具调用合法率从 71% 拉到 96%——这一步很成功。
三个月后,业务方提新需求:回答语气太生硬,要「更亲切、更像人工客服」。你于是又准备了一份 8000 条的语气风格数据,同样从 base 出发再训一个 LoRA。结果上线前评估发现:语气上去了,工具调用格式反而崩了一半(JSON 里开始混自然语言解释),因为你不可能在语气数据里覆盖所有工具调用场景。
接下来团队通常会走这条路:把两份数据混在一起,跑第三次 SFT。这条路的成本是整个微调生命周期里最贵的一环——数据要重新清洗配比、训练要重新排队 4~8 小时、学习率要重调,训完往往还发现「工具调用 92%,语气只是勉强到 4 分(5 分制)」,两头都不如专门的单任务模型。更麻烦的是这次成功不可复用:下周再来第三个能力(SQL 生成),你又要重跑一次三合一训练,而且很可能把前两个能力再吃掉一点,这就是**灾难性遗忘(Catastrophic Forgetting)**在工程上的真实样子。
本文只讲一个单点技术:模型合并(Model Merging)。它让你把已经训好的多个 checkpoint,在权重空间里直接做算术运算,零训练(或几分钟 CPU 时间)得到一个同时具备多种能力的单一模型,推理成本与单模型完全相同。我会把数学讲透、代码给全、数据摆出来,并明确告诉你它在什么情况下会悄悄失效。
技术背景与核心概念扫盲
从 LoRA 的 delta 说起
LoRA(Low-Rank Adaptation,低秩适配)的核心是冻结原权重 $W_0$,只训练一对低秩矩阵 $B \in \mathbb{R}{d \times r}$、$A \in \mathbb{R}{r \times k}$:
$$W = W_0 + \frac{\alpha}{r} BA$$
其中 $\frac{\alpha}{r}$ 是缩放系数(scaling),r 是秩。绝大多数人熟悉的是「推理时把 LoRA 挂上去」,但从合并的视角看,LoRA 真正教给模型的东西是那个增量 $\Delta W = \frac{\alpha}{r}BA$。
任务向量:让「能力」变成一个可以运算的向量
2022 年的 Model Soup 论文(Wortsman et al.)证明了同一模型多次微调的权重做平均,性能不降反升;2023 年 ICLR 的 Task Arithmetic 论文(Ilharco et al.)把它形式化为任务向量(Task Vector):
$$\tau_i = \theta_{\text{finetuned},i} - \theta_{\text{base}}$$
于是「合并两个能力」就变成:
$$\theta_{\text{merged}} = \theta_{\text{base}} + \alpha \sum_i \tau_i$$
这个公式看起来像作弊,但它有效的原因并不神秘:微调对权重的改动是结构化且低幅的,$\tau$ 携带的是「往某个能力方向平移」的语义信息,而不是随机噪声。既然有结构,就可以相加减——甚至可以用负号减掉某个能力(比如「遗忘某类回答」)。
一条必须记住的硬约束
$$\text{架构相同} + \text{分词器相同} + \text{同一 base} \Rightarrow \text{可以合并}$$
三条缺一不可。7B 和 13B 合并是无意义的;Qwen3 和 Llama 3.1 合并是无意义的;同一个 base 但一个改过词表(加了特殊 token)也会出问题(后文踩坑章节详述)。这是模型合并在工程上最重要的一条边界。
2026 年的技术地图
模型合并在 2026 年已经不是「kaggle 技巧」,而是一条独立的技术分支:
| 方法 | 来源 | 解决什么问题 | 适合模型数 |
|---|---|---|---|
| Linear / Model Soup | Wortsman 2022 | 同源模型平均,降方差 | 2+ |
| SLERP | 经典球面插值 | 线性平均导致的范数坍缩 | 恰好 2 |
| Task Arithmetic | Ilharco, ICLR 2023 | 能力的加减法 | 2+ |
| TIES-Merging | Yadav, NeurIPS 2023 | 符号冲突与低幅噪声干扰 | 2+(≥3 才有意义) |
| DARE | Yu et al. 2024 | 增量参数极度冗余 | 任意(预处理) |
| DELLA | 2024 | 按行幅度自适应剪枝,比 DARE 温和 | 2+ |
| Model Stock | 2024 | 自动推算最优插值系数 t | 3+ |
| NuSLERP / Multi-SLERP / Karcher | mergekit 0.1+ | 多模型球面重心 | 2+ |
| RAM | 2026 | RL 训练产生的稀疏异质任务向量 | 2+ |
| Evolutionary Merge | Sakana AI, 2024 | 自动搜索权重/层组合 | 2+ |
配套的评测基准也在补齐:MergeBench(arXiv 2505.10833)专门评测领域专家模型的合并效果,ICLR 2026 出现了《Expert Merging》(无监督对齐 + 按层重要性分块合并)与《Merge Before Forget》(单 LoRA 持续学习)两项工作,ACM Computing Surveys 2026 也收录了这一方向的系统性综述。工具侧,Arcee AI 的 mergekit 已经是事实标准(GitHub 7.4k star),支持 20 种合并方法、纯 CPU 运行、分片惰性加载(--lazy-unpickle),8GB 显存就能干活。
一句话总结它的性价比:mergekit 论文统计,HuggingFace Open LLM Leaderboard 前 50 名里有 20%、前 100 名里有 34% 是合并模型,而这些模型的训练成本接近零。
底层原理深度拆解
干扰从哪来:两类失效机制
把两个 $\tau$ 直接相加,性能会掉,原因是两类干扰(TIES 论文的表述):
- 冗余低幅参数带来的噪声:$\tau$ 里绝大部分分量幅度极小,对任务没贡献,但合并时会被叠加成噪声,稀释真正的信号;
- 符号(sign)不一致:同一个权重位置,模型 A 把它 +0.002,模型 B 把它 −0.003,朴素平均得到 −0.0005 —— 两个方向的信息同时被抹掉,还引入了第三个谁都没训过的值。
第二类才是致命的。它解释了为什么「合并两个模型」经常比预期差:你以为在做加法,实际上在做抵消。
TIES 的三步:裁剪 → 选符号 → 不相交合并

图 1:TIES 先按幅度裁剪每个任务向量,再逐位置用电票选出统一符号,最后只对符号一致的参数取平均。
flowchart LR
A["τ₁ = θ₁ - θ_base"] --> T1["Trim: 只保留幅度 top-k%<br/>(density=0.5)"]
B["τ₂ = θ₂ - θ_base"] --> T2["Trim: 只保留幅度 top-k%"]
C["τ₃ = θ₃ - θ_base"] --> T3["Trim: 只保留幅度 top-k%"]
T1 --> E["Elect Sign: 逐位置按幅度加权<br/>选出多数派符号"]
T2 --> E
T3 --> E
E --> D["Disjoint Merge: 只平均<br/>符号一致的存活参数"]
D --> M["θ_merged = θ_base + λ·Στ"]
- Trim(裁剪):每个任务向量按绝对值排序,只保留最大的
density比例,其余置零。经验值 0.2 就够——论文与社区实践都表明,只留 20% 参数性能几乎不掉。 - Elect Sign(选符号):在每个参数位置,把各模型该位置的幅度按符号分组求和,谁的总幅度大就用谁的符号;符号落选的模型在该位置被清零。
- Disjoint Merge(不相交合并):只对「存活且符号一致」的参数求平均。
这就是为什么 TIES 更适合三个以上模型:两个模型时符号冲突相对少,SLERP 更简单也够用;三五个模型时冲突急剧上升,TIES 的收益才开始显现。
DARE:为什么丢掉 99% 的增量还能不掉点
DARE(Drop And REscale)的逻辑更激进:以概率 $p$ 随机丢弃任务向量里的参数,把存活的参数乘以 $\frac{1}{1-p}$ 以保持期望不变:
$$\tau' = \frac{m \odot \tau}{1-p}, \quad m_i \sim \text{Bernoulli}(1-p)$$
数学上这是一个无偏估计($\mathbb{E}[\tau'] = \tau$),因此剪掉绝大部分参数不会系统性改变方向,只是方差变大一点点。论文的实测结论是:即使丢弃 90%~99% 的微调更新,性能依然保持。这条结论的工程含义非常大——你的 LoRA 里大部分参数是冗余的。
在 mergekit 里,DARE 通常作为预处理步骤与 TIES 组合,配置写成 dare_ties;density 是保留比例,rescale: true 打开重缩放。
SLERP 与范数坍缩
线性平均 $\theta = (1-t)\theta_A + t\theta_B$ 在高维空间会把结果的范数压小——两个方向不完全一致的向量取中点,长度一定短于两者。表现就是合并模型「变钝」:输出更保守、风格更平、困惑度上升。
SLERP(Spherical Linear Interpolation,球面线性插值)改为沿单位超球面的测地线插值:
$$\theta(t) = \frac{\sin((1-t)\omega)}{\sin\omega}\theta_A + \frac{\sin(t\omega)}{\sin\omega}\theta_B, \quad \omega = \arccos\frac{\langle \theta_A, \theta_B\rangle}{|\theta_A||\theta_B|}$$
它保持范数恒定,所以在「恰好两个模型、同一 base」的场景下是默认首选。限制也很明确:一次只能合两个,三个以上要么链式两两合并(层级合并),要么换 TIES/task arithmetic。
权重空间合并 vs 输出空间集成

图 2:集成(Ensemble)走 N 次前向;合并(Merge)只产出一份权重,推理开销与单模型一致。
flowchart TB
subgraph ENS["输出空间集成(Ensemble)"]
I1["请求"] --> M1["模型 A"] --> V1["投票/加权融合"]
I1 --> M2["模型 B"] --> V1
I1 --> M3["模型 C"] --> V1
V1 --> O1["N 倍算力 + N 倍显存"]
end
subgraph MRG["权重空间合并(Merge)"]
W["θ_base"] --> P["θ_base + λ(τ_A + τ_B + τ_C)"]
TA["τ_A"] --> P
TB["τ_B"] --> P
TC["τ_C"] --> P
P --> O2["1 个模型 / 1 次前向 / 原始延迟"]
end
图 3:同一业务目标的两种工程路径。大规模推理场景下,集成的经济性会迅速崩掉。
对每天千万次请求的系统来说,5 模型集成就等于 5 倍推理账单;合并模型是 1 倍。代价是精度上限略低——集成靠方差抵消,合并靠权重叠加,后者无法完全复刻前者。但在绝大多数业务场景里,这个差距远小于「省下的 80% 推理成本」。
什么时候合并注定失败:损失盆地
微调过程有随机性:学习率、数据顺序、正则强度不同,两个 checkpoint 可能落在损失曲面的不同盆地(basin)里。此时在两个 $\theta$ 之间做线性插值,中途会穿过高损失区域——现象是「合并模型在所有任务上都比两个父模型差」,而不只是某一个任务掉。模式连通性(mode connectivity)研究显示:同一 base、相似超参、相似数据规模训出来的模型,这种风险较低;一旦某个父模型用了差异极大的训练配方(比如全参微调 3 个 epoch),就要小心了。
工程上有个便宜的预检方法:直接在两个 checkpoint 之间做 $t \in {0.1, 0.3, 0.5, 0.7, 0.9}$ 的线性插值,看中间点的困惑度是否断崖式上升。断崖 = 盆地不兼容 = 别合并,去训第三个模型或者把两个 LoRA 分开服务。
手把手实战落地
环境:一台 32 vCPU / 64GB 内存的机器(合并阶段纯 CPU 即可)+ 一张训练用 GPU。Python 3.11,mergekit、peft、transformers、trl、lm-evaluation-harness。

图 4:一次可复现的合并流水线——训练 → 导出全量权重 → 干扰预检 → 合并 → 三档评测 → 灰度发布。
flowchart TD
S1["① 从同一 base 训练 N 个 LoRA<br/>(tool / tone / sql)"] --> S2["② peft merge_and_unload<br/>导出全量 bf16 checkpoint"]
S2 --> S3["③ 任务向量预检<br/>cos(τ_i, τ_j) 与范数比"]
S3 -->|"cos < 0.1 且范数接近"| S4["④ mergekit 合并<br/>SLERP / dare_ties"]
S3 -->|"cos 接近 0 或负、范数悬殊"| S9["放弃合并<br/>回退第三次 SFT 或多 LoRA 服务"]
S4 --> S5["⑤ 三档评测<br/>通用 / 域内 / LLM-as-judge"]
S5 -->|"任一能力 < 父模型 5pp"| S4
S5 -->|"通过门禁"| S6["⑥ 灰度 5% → 50% → 全量"]
Step 0:安装
# mergekit 官方推荐从源码安装(合并本身是 CPU 密集型,不需要 GPU)
git clone https://github.com/arcee-ai/mergekit.git
cd mergekit && pip install -e ".[evolve]" # evolve 额外拉入 optuna 等进化搜索依赖
# 训练与评测侧
pip install -U "transformers>=4.53" "peft>=0.17" "trl>=0.24" accelerate datasets
pip install -U lm-evaluation-harness
mergekit-yaml --help # 验证入口
Step 1:从同一个 base 训练两个 LoRA(代码 1)
合并的前提是同一 base,所以训练脚本里 BASE 必须是同一个 revision,这一点后面要在配置里显式锁住。
# train_lora.py —— 同一 base、不同数据、不同任务,各训一个 LoRA
import torch
from datasets import load_dataset
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
BASE = "Qwen/Qwen3-4B-Instruct-2507" # 合并的前提:所有父模型共用这个 base
OUT = "./ckpt/qwen3-4b-tool-sft" # 换数据时只改这里的输出目录
# r 不要盲目调大:r 越大 ΔW 幅度越大,跨任务干扰越强(后文踩坑 3)
lora_cfg = LoraConfig(
r=16,
lora_alpha=32, # alpha/r = 2.0 的缩放系数
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"], # 覆盖全部线性层,合并时更稳
task_type="CAUSAL_LM",
)
args = SFTConfig(
output_dir=OUT,
num_train_epochs=3,
learning_rate=1e-4,
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # 等效 batch = 16
bf16=True,
max_length=4096,
logging_steps=10,
save_strategy="epoch",
report_to="none",
)
# 数据用 messages 字段(chat 格式),trl 会自动套 chat_template
ds = load_dataset("your-org/tool-use-sft", split="train")
trainer = SFTTrainer(
model=BASE,
args=args,
train_dataset=ds,
peft_config=lora_cfg,
)
trainer.train()
trainer.save_model(OUT) # 只保存 adapter + tokenizer
同一脚本换 ds 与 OUT 跑两遍,得到 tool-sft 与 tone-sft;如果有第三个能力,再加 sql-sft。
Step 2:把 LoRA 导出成全量 checkpoint(代码 2)
mergekit 做的是全量权重算术,不能直接吃 adapter(虽然它有 mergekit-extract-lora 反向提取,但那不是我们的路径)。所以必须先「合入」全量权重。
# export_full.py —— 把 adapter 合入 base,导出可被 mergekit 消费的全量权重
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
BASE = "Qwen/Qwen3-4B-Instruct-2507"
ADAPTER = "./ckpt/qwen3-4b-tool-sft"
OUT = "./full/qwen3-4b-tool-sft"
tok = AutoTokenizer.from_pretrained(ADAPTER) # adapter 目录里有 tokenizer
model = AutoModelForCausalLM.from_pretrained(
BASE, torch_dtype=torch.bfloat16, device_map="cuda"
)
model = PeftModel.from_pretrained(model, ADAPTER)
# safe_merge=True:内部用 float32 计算并检测 NaN/Inf,避免 fp16 下溢导致的坏权重
merged = model.merge_and_unload(safe_merge=True)
merged.save_pretrained(OUT, safe_serialization=True, max_shard_size="4GB")
tok.save_pretrained(OUT) # tokenizer 必须一起存,否则模板会丢
print("导出完成:", OUT)
这一步不要偷懒用 fp16。 fp16 在加 $W_0 + \Delta W$ 时容易溢出为 inf,safe_merge=True 会强制走更稳的精度路径(代价是慢一些),这是踩过坑之后必须打开的开关。
Step 3:合并前先做任务向量干扰预检(代码 3)
不要盲目合并。先花两分钟算一下两个父模型的任务向量余弦相似度和范数比,这能提前拦掉 80% 的失败合并。
# precheck.py —— 计算任务向量余弦相似度与范数比,判断是否值得合并
import glob, os
import torch
from safetensors import safe_open
def _handles(path):
"""返回某目录下所有 safetensors 分片的 (文件名, 句柄) 列表(惰性,不整包加载)"""
files = sorted(glob.glob(os.path.join(path, "*.safetensors")))
return [(f, safe_open(f, framework="pt", device="cpu")) for f in files]
def _get(handles, key):
for _, h in handles:
if key in h.keys():
return h.get_tensor(key)
raise KeyError(key)
def delta_stats(base_dir, a_dir, b_dir, limit=None):
hb, ha, hbb = _handles(base_dir), _handles(a_dir), _handles(b_dir)
keys = list(hb[0][1].keys()) # 简化:只扫首个分片的键(示例规模足够)
if limit:
keys = keys[:limit]
dot = na = nb = 0.0
for k in keys:
w0 = _get(hb, k).float()
ta = _get(ha, k).float() - w0 # τ_A
tb = _get(hbb, k).float() - w0 # τ_B
dot += torch.sum(ta * tb).item()
na += torch.sum(ta * ta).item()
nb += torch.sum(tb * tb).item()
cos = dot / (na ** 0.5 * nb ** 0.5 + 1e-12)
return {"cosine": round(cos, 4),
"norm_a": round(na ** 0.5, 2),
"norm_b": round(nb ** 0.5, 2),
"norm_ratio": round((nb ** 0.5) / (na ** 0.5 + 1e-12), 3)}
print(delta_stats("./full/qwen3-4b-base",
"./full/qwen3-4b-tool-sft",
"./full/qwen3-4b-tone-sft"))
判读规则(这是经验口径,不是定律):
cosine在 0.10.6 且2.0:适合合并,冲突温和;norm_ratio在 0.5cosine接近 0 但两者范数都很小:也还行,属于「正交的独立技能」;cosine接近 1 且范数接近:两个父模型高度相似,直接 linear/SLERP 就好,不必上 TIES;cosine为负,或norm_ratio> 5:强干扰信号,先怀疑某个父模型训过头了(epoch 太多、学习率太大),合并大概率掉点。
Step 4:两个模型用 SLERP(代码 4)
# slerp_tool_tone.yaml —— 恰好两个模型,共享 base,用分层 t 控制谁的风格占主导
slices:
- sources:
- model: your-org/qwen3-4b-tool-sft
layer_range: [0, 36] # Qwen3-4B 共 36 层,必须与 config.json 一致
- model: your-org/qwen3-4b-tone-sft
layer_range: [0, 36]
merge_method: slerp
base_model: your-org/qwen3-4b-tool-sft # SLERP 必填,t=0 即该模型,t=1 为另一侧
parameters:
t: # 支持按张量名过滤,并沿层做渐变
- filter: self_attn # 注意力层:浅层偏 tool,深层偏 tone
value: [0.0, 0.5, 0.3, 0.7, 1.0]
- filter: mlp # MLP 层:反向分配
value: [1.0, 0.5, 0.7, 0.3, 0.0]
- value: 0.5 # embedding / norm 等其余参数:五五开
dtype: bfloat16
# 合并(CPU 即可跑,32 核机器上 4B 模型约 5~8 分钟)
mergekit-yaml slerp_tool_tone.yaml ./merged/qwen3-4b-slerp \
--lazy-unpickle --copy-tokenizer --out-shard-size 4B
# 显存够、想快一点就加 --cuda(mergekit 支持 8GB 显卡做惰性流式合并)
# mergekit-yaml slerp_tool_tone.yaml ./merged/qwen3-4b-slerp --cuda --lazy-unpickle
t 的渐变(gradient)是 mergekit 里最实用的旋钮:value: [0.0, 0.5, 0.3, 0.7, 1.0] 表示从浅层到深层按这五个锚点线性插值。实践中「跨层变化 t」往往明显优于全局单一系数——因为浅层更偏通用语法、深层更偏任务行为。
Step 5:三个以上模型用 DARE-TIES(代码 5)
# dare_ties_three.yaml —— 三个专家 + 一个 base,DARE 剪枝后再做 TIES 选符号
models:
- model: your-org/qwen3-4b-base # base 不需要 parameters
- model: your-org/qwen3-4b-tool-sft
parameters:
density: 0.5 # 只保留 50% 幅度最大的增量参数
weight: 0.5
- model: your-org/qwen3-4b-tone-sft
parameters:
density: 0.5
weight: 0.35
- model: your-org/qwen3-4b-sql-sft
parameters:
density: 0.45
weight: 0.3
merge_method: dare_ties
base_model: your-org/qwen3-4b-base
tokenizer:
source: base # 词表对齐策略:base / union / 具体路径
chat_template: auto # 对话模板沿用父模型,防止模板丢失
parameters:
normalize: true # 权重和自动归一化到 1
rescale: true # DARE 的重缩放 1/(1-p),务必打开
int8_mask: true # 掩码用 int8 存储,省内存
dtype: bfloat16
mergekit-yaml dare_ties_three.yaml ./merged/qwen3-4b-dareties --lazy-unpickle
几个容易踩的数值细节:
weight之和最好落在 0.9~1.1 之间;设了normalize: true会自动归一,但如果和为 0 会直接给你 NaN。- 合并后如果「整体变平淡」,先把
density提到 0.6~0.7;如果「某个能力被吃掉」,反而要降低该模型的density并提高weight。 dtype: bfloat16时 mergekit 内部仍会用 float32 计算再回转(Linear/SLERP 路径如此),不用自己担心精度。
Step 6:三档评测与代码 6
合并最大的陷阱是「看聚合分数涨了就发版」。正确姿势是三档评测:通用回归、域内能力、开放问答。先用 lm-evaluation-harness 做通用回归:
# 通用能力回归:与 base、两个父模型跑同一套任务、同一口径
for M in ./full/qwen3-4b-base ./full/qwen3-4b-tool-sft \
./full/qwen3-4b-tone-sft ./merged/qwen3-4b-dareties; do
lm_eval --model hf \
--model_args pretrained=$M,dtype=bfloat16 \
--tasks mmlu,gsm8k --num_fewshot 5 --batch_size 8 \
--output_path ./eval/$(basename $M).json
done
域内能力必须自己写,因为工具调用合法率、语气这类指标 harness 里没有:
# domain_eval.py —— 域内三指标:工具调用合法率 / SQL 可执行率 / 语气 LLM-as-judge
import json, re
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
def load(model_path, adapter=None):
tok = AutoTokenizer.from_pretrained(model_path)
m = AutoModelForCausalLM.from_pretrained(model_path, dtype="bfloat16", device_map="cuda")
if adapter: # 父模型是 adapter 时临时挂载
m = PeftModel.from_pretrained(m, adapter)
return tok, m
def gen(tok, model, messages, max_new_tokens=512):
prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
ids = tok(prompt, return_tensors="pt").to(model.device)
out = model.generate(**ids, max_new_tokens=max_new_tokens, do_sample=False)
return tok.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True)
TOOL_RE = re.compile(r'\{\s*"name"\s*:\s*"[^"]+"\s*,\s*"arguments"\s*:\s*\{.*?\}\s*\}', re.S)
def score_tool_call(text):
"""工具调用合法率:必须能被 JSON 解析且含 name/arguments 两个键"""
m = TOOL_RE.search(text)
if not m:
return 0
try:
obj = json.loads(m.group(0))
return int(isinstance(obj.get("name"), str) and isinstance(obj.get("arguments"), dict))
except json.JSONDecodeError:
return 0
def run(model_path, cases_path, adapter=None):
tok, model = load(model_path, adapter)
cases = [json.loads(l) for l in open(cases_path, encoding="utf-8")]
hits = [score_tool_call(gen(tok, model, c["messages"])) for c in cases]
print(f"{model_path}: 工具调用合法率 = {sum(hits)/len(hits):.3f} (n={len(hits)})")
if __name__ == "__main__":
run("./merged/qwen3-4b-dareties", "./data/tool_cases.jsonl") # 对 base 与各父模型重复一次
语气这类主观指标用 LLM-as-judge 成对比较(pairwise)比打绝对分更稳:把合并模型与父模型 B 的回答随机左右交换位置,让判官模型选更自然的一个,统计胜率。注意判官也要做位置偏差校准——同一批样本左右各跑一次,只统计两次一致的结果。
Step 7:自动搜索最优合并配方(代码 7)
手工调 density / weight 是体力活。mergekit 自带的进化合并(Evolutionary Merge,Sakana AI 的思路工程化版本)用 Optuna 在权重空间 + 层组合空间里搜索:
# evolve.yaml —— 以综合分为目标,自动搜索每个模型的 weight 与分层系数
genome:
models:
- your-org/qwen3-4b-tool-sft
- your-org/qwen3-4b-tone-sft
- your-org/qwen3-4b-sql-sft
merge_method: dare_ties
base_model: your-org/qwen3-4b-base
layer_granularity: 8 # 每 8 层共享一组权重,粒度越细搜索空间越大
normalize: true
filters:
- filter: self_attn
value: [0.0, 0.25, 0.5, 0.75, 1.0]
- filter: mlp
value: [1.0, 0.75, 0.5, 0.25, 0.0]
tasks:
- gsm8k
- mmlu
mergekit-evolve evolve.yaml \
--storage-path ./evol --batch-size 4 -j 4 --gpu \
--max-fevals 120 # 默认会一直跑到 100 次评估,建议显式设定上限
# 结束后最优配置在 ./evol/best_config.yaml,再用 mergekit-yaml 生成最终模型
mergekit-yaml ./evol/best_config.yaml ./merged/qwen3-4b-best --lazy-unpickle
进化搜索的价值不在于「多聪明」,而在于把人工试错变成可重复的流水线。Sakana 当年的经典案例是用一个日语模型和一个英语数学模型合并,意外得到日语数学推理能力——这类非直觉组合手工极难想到。
关键细节与踩坑指南
坑 1:tokenizer 与特殊 token 错位
两个父模型都往词表里加了 <|tool_call|>,而且都加在第 151,900 号位置——但语义完全不同。合并后这个位置的 embedding 被平均,行为变成随机。修法:合并前用 resize_token_embeddings 统一词表,或在 YAML 里显式指定 tokenizer: {source: union}。最省事的做法是:微调阶段就不要加新特殊 token,用结构化文本(JSON)表达工具调用。
坑 2:chat_template 丢失,格式全崩
合并脚本只认权重,tokenizer_config.json 里的 chat_template 可能来自错误的父模型(甚至为空)。真实现象是:模型答得没错,但 apply_chat_template 出来的 prompt 和你训练时的格式不一致,于是输出全是散的。所以 YAML 里务必写 chat_template: auto,合并后手工 diff 一次:
from transformers import AutoTokenizer
for p in ["./full/qwen3-4b-tool-sft", "./merged/qwen3-4b-dareties"]:
t = AutoTokenizer.from_pretrained(p)
print(p, "chat_template 长度 =", len(t.chat_template or ""), "| vocab =", t.vocab_size)
坑 3:LoRA 的 r 开太大
r=128 训出来的 $\Delta W$ 幅度大、方向更「硬」,合并时干扰显著变强。合并场景下的经验是 r 控制在 8~32、lora_alpha/r 不大于 2;如果确实需要大容量,宁可换成 TIES + 更低 density,也别指望大 r 还能干净合并。
坑 4:拿量化模型去合并
AWQ / GPTQ 的 int4 权重不能直接做任务向量算术——量化误差会被当作 $\tau$ 的一部分参与平均。必须用未量化的全量 bf16/fp32 权重合并,合并完再量化发布。
坑 5:fp16 合并出 NaN
前面提过,这里给对比:
| 错误写法 | 正确写法 | 后果差异 |
|---|---|---|
mergekit-yaml ... 默认 dtype,父模型是 fp16 |
dtype: bfloat16 + merge_and_unload(safe_merge=True) |
fp16 相加溢出 → 权重变 inf/nan,模型只会输出「……」 |
normalize: true 但 weight 全为 0 |
保证 weight 和 ∈ [0.9, 1.1] |
归一化除以 0 → 直接 NaN |
忽略 rescale:rescale: false |
rescale: true(DARE 默认应开) |
丢掉 95% 参数却不放大,输出幅度整体缩小,模型变「气虚」 |
坑 6:只看聚合分数,掩盖单能力回退
MMLU 从 68.2 掉到 67.1 看似在噪声范围内(1000 样本抽样,95% 置信区间约 ±1.5pp),但工具调用合法率可能已经从 96% 掉到 88%。必须逐能力对比,且每个能力都和它最强的那个父模型比,而不是和 base 比。
坑 7:继承双亲的缺陷,还叠加了
如果父模型 A 爱瞎编参数、父模型 B 有过度拒答倾向,合并模型通常会同时具备这两类毛病。缺陷不像能力那样「平均抵消」,因为它们在权重空间里是正交方向的扰动。上线前要针对每个父模型的已知故障做定向红队,别假设合并会把问题抹平。
坑 8:安全对齐被「合掉」
这是最危险的一类。EMNLP 2024 Findings 的《Model Merging and Safety Alignment》给出实测:把专家模型合并后,安全对齐分数会显著下降;用 DARE-TIES + 进化合并(EvoMM)且把安全数据作为独立专家一起合并,安全分才能回到 96.1(原始 LLaMA 为 98.0);不带安全数据时只有 80.1。结论很直白:合并必须显式包含一个安全/对齐专家,否则等于把护栏一起平均掉。
坑 9:合并产物不可追溯
半年后没人记得这个 checkpoint 是怎么来的。强制要求:合并 YAML 与产物一起入库,mergekit 会自动生成带配置的 README.md,别删。生产环境要能回答「哪个 base revision + 哪几个父模型 + 什么参数」三个问题。
生产环境最佳实践
合并作为流水线,而不是一次性脚本
# merge_pipeline.yaml —— CI 门禁:不达标直接阻断发布
pipeline:
base: {repo: "Qwen/Qwen3-4B-Instruct-2507", revision: "main"} # 锁 revision!
experts:
- {name: tool, repo: "your-org/qwen3-4b-tool-sft", revision: "v3"}
- {name: tone, repo: "your-org/qwen3-4b-tone-sft", revision: "v2"}
- {name: sql, repo: "your-org/qwen3-4b-sql-sft", revision: "v1"}
- {name: safety, repo: "your-org/qwen3-4b-safety-sft", revision: "v5"} # 安全专家必须有
merge:
method: dare_ties
density: {tool: 0.5, tone: 0.5, sql: 0.45, safety: 0.6}
weight: {tool: 0.5, tone: 0.3, sql: 0.25, safety: 0.35}
normalize: true
dtype: bfloat16
gates: # 三条硬门禁
general_mmlu_drop_max_pp: 2.0 # 通用能力下降不超过 2 个百分点
per_task_drop_max_pp: 3.0 # 单任务相对最强父模型下降不超过 3 个百分点
tool_call_valid_rate_min: 0.97 # 工具调用合法率绝对下限
artifacts: # 产物留档,用于回溯与再合并
- merged_model
- merge_config.yaml
- eval_report.json
监控指标(上线后 30 天必须盯)
| 维度 | 指标 | 告警阈值(示例) |
|---|---|---|
| 结构合规 | 工具调用 JSON 解析失败率 | > 1.5% 持续 10 分钟 |
| 行为漂移 | 平均输出长度突增/突降 | 偏离基线 ±25% |
| 拒答 | 拒答率 | 相对基线 +5pp |
| 幻觉 | 引用不存在字段/编号 | > 0.5% |
| 成本 | 每会话 token 数 | 相对基线 +20% |
| 对比 | 影子流量下与父模型 A/B 胜率 | 合并模型胜率 < 45% |
成本对比(这是模型合并的全部意义)
引用可查的 GPU 价格口径(Spheron 2026-11-09 按需价:H100 $2.98/h、A100 $1.48/h、RTX 4090 $0.48/h):
| 路径 | 机器时间 | 直接成本 | 可复用性 |
|---|---|---|---|
| mergekit 合并(4B 模型,3 专家) | 32 vCPU 上 5~8 分钟 | ≈ $0.05(或本地电费) | 配置入库,随时重跑 |
| 第三次联合 SFT(4B,QLoRA) | 2×A100 × 4.5h | ≈ $13.3 + 数据准备人天 | 一次性的,加新能力要重来 |
| 维持 3 个独立端点 | 3× 常驻显存 | 3× 推理账单 | 无遗忘,但运维成本高 |
对 4B 这个量级,合并的成本优势已经是 100 倍以上;模型越大差距越夸张,因为训练是超线性的,而合并只是逐张量读写。
合并 vs 多 LoRA 服务:别忘了第三条路
如果你的多人格/多租户需求只是路由不同(客户 A 用风格 X,客户 B 用风格 Y),那不一定要合并——vLLM 的多 LoRA 服务(--enable-lora --max-loras 4 --max-lora-rank 64)在显存上更划算,且完全不损失任何单一能力。什么时候该选哪条:
- 需要一个模型同时具备多种能力(比如既会调工具又要讲人话)→ 合并;
- 需要按请求切换不同专长(不同租户/不同任务类型)→ 多 LoRA 动态加载;
- 需要极致单任务精度且流量不大 → 直接单任务模型,别折腾。
横向对比与选型建议
| 方法 | 输入模型数 | 核心机制 | 主要解决的失效模式 | 典型参数 | 什么时候别用 |
|---|---|---|---|---|---|
linear |
2+ | 加权平均 | 同源模型降方差 | weight, normalize |
两个模型差异大时会范数坍缩 |
slerp |
恰好 2 | 球面测地线插值 | 范数坍缩 | t(可分层渐变) |
3 个以上模型 |
nuslerp |
2 | 在任务向量空间做 SLERP | SLERP 的灵活性与速度 | t, nuslerp_row_wise |
需要多模型一次合并 |
multislerp / karcher |
2+ | 切空间/流形重心 | 多模型几何均值 | weight, max_iter |
需要稀疏化抗干扰时 |
task_arithmetic |
2+ | $\theta_b + \lambda\sum\tau_i$ | 能力加减 | weight, lambda |
模型数 > 4,干扰累积快 |
ties |
2+(≥3 才划算) | 裁剪 + 选符号 + 不相交合并 | 符号冲突 | density, weight |
只有两个高度相似模型 |
dare_ties |
2+ | DARE 随机剪枝 + TIES 选符号 | 冗余参数 + 符号冲突 | density, rescale: true |
父模型增量极小(几百个参数)时会伤 |
della |
2+ | 按行幅度自适应剪枝 | DARE 随机性带来的抖动 | density, epsilon |
追求最快跑通时先别用 |
breadcrumbs_ties |
2+ | 同时剪掉极小与极大增量 | 离群大改动污染 | density, gamma |
父模型含关键的大幅改动 |
model_stock |
3+ | 用余弦相似度推算插值系数 | 手调 t | filter_wise |
只有两三个差异极大的模型 |
ram |
2+ | 区分「独有/共享/未激活」参数 | RL 训练产生的稀疏异质增量 | epsilon, r, alpha |
SFT 出来的稠密任务向量 |
passthrough |
1/切片 | 直接拼层(Frankenmerge) | 层深不足 | slices, scale |
需要能力融合,不是加深 |
| 输出集成 | 2+ | 多模型投票 | 方差 | — | 高 QPS、成本敏感场景 |
| 多 LoRA 服务 | 1 base + N adapter | 运行时切换 | 无遗忘 | max_loras |
需要单模型同时具备多能力 |
选型决策路径(可以直接照着走):
flowchart TD
Q0{"父模型是否同架构、同 tokenizer、同 base?"} -->|"否"| N1["放弃合并<br/>走第三次 SFT 或多端点"]
Q0 -->|"是"| Q1{"你有几个父模型?"}
Q1 -->|"恰好 2"| Q2{"两者能力方向差异大吗?<br/>(cos(τ) < 0.4)"}
Q2 -->|"不大"| A1["SLERP / linear<br/>t 做分层渐变"]
Q2 -->|"很大"| A2["task_arithmetic<br/>或 dare_ties(density 0.4~0.5)"]
Q1 -->|"3 个及以上"| Q3{"合并后是否有能力被吃掉?"}
Q3 -->|"是"| A3["dare_ties / della<br/>降低弱模型的 density"]
Q3 -->|"否,但想再压分"| A4["mergekit-evolve<br/>自动搜 weight 与分层系数"]
A3 --> Q4{"通用能力是否下降 > 2pp?"}
A4 --> Q4
Q4 -->|"是"| N2["提高 density 或回退<br/>怀疑损失盆地不兼容"]
Q4 -->|"否"| P["进入 CI 门禁 + 灰度"]
性能实测与效果验证
先看外部可查的量化结论(这些是选型依据的底盘):
| 来源 | 结论 |
|---|---|
| Arcee mergekit 论文(arXiv 2403.13257) | Open LLM Leaderboard 前 50 名中 20%、前 100 名中 34% 为合并模型 |
| TIES 论文(arXiv 2306.01708) | 合并模型数到 7 个时,最佳方法约保留单任务 SFT 模型 85% 的性能;2 个模型时几乎无损 |
| DARE 论文(arXiv 2311.03099) | 丢弃 90%~99% 的任务向量参数,性能仍可保持;WizardLM-13B + WizardMath-13B 的合并版在指令跟随与 GSM8K 上可同时超越两个父模型 |
| Model Merging & Safety(EMNLP 2024 Findings) | DARE-TIES + EvoMM 且含安全专家:安全分 96.1(LLaMA 原 98.0);不含安全专家仅 80.1 |
| HF automerger 数据分析 | SLERP 在「坏模型」尾部略少、DARE-TIES 在「好模型」长尾略优,两者无压倒性差异 |
再给一份我们本地复现的记录(Qwen3-4B,三个 LoRA,r=16、alpha=32,评测集:工具调用 600 条、语气成对判官 400 组、SQL 执行 500 条、MMLU 1000 条抽样)。单次运行,数字用于相对比较,不作为绝对基准:
| 模型 | 工具调用合法率 | 语气胜率(对 base) | SQL 执行准确率 | MMLU | 合并/训练耗时 |
|---|---|---|---|---|---|
| base | 71.2% | — | 31.5% | 68.4 | — |
| 父模型 A(tool) | 96.5% | 38% | 42.0% | 68.2 | 3.1h / A100 |
| 父模型 B(tone) | 61.0% | 74% | 40.5% | 67.9 | 2.4h / A100 |
| 父模型 C(sql) | 58.3% | 41% | 81.4% | 67.6 | 2.7h / A100 |
| SLERP(A, B) | 90.1% | 69% | 43.2% | 66.8 | 6.4min / CPU |
| DARE-TIES(A,B,C) | 93.4% | 64% | 74.9% | 67.1 | 7.2min / CPU |
| 第三次联合 SFT | 92.0% | 71% | 76.2% | 66.4 | 4.5h / 2×A100 |
xychart-beta
title "相对各自最强父模型的保持率(本地复现,%)"
x-axis ["工具调用", "语气", "SQL", "MMLU"]
y-axis "保持率 %" 88 --> 102
bar [96.8, 86.5, 92.0, 98.4]
bar [95.3, 95.9, 93.6, 97.4]
图 5:DARE-TIES 合并(第一组柱)与第三次联合 SFT(第二组柱)的能力保持率对比。合并几乎不用训练时间,却在工具调用上更稳(93.4% vs 92.0%),代价是语气略弱。
三个值得注意的现象:
- 合并的「短板」是可预测的:DARE-TIES 在语气上只有 64% 胜率,因为语气是最主观、最容易在平均中被磨平的能力。这不是 bug,而是权重平均的固有倾向(它倾向于保留「可验证」的能力,比如 JSON 格式、SQL 语法)。
- 合并的通用能力损失约 1.1pp,与第三次 SFT 的 2.0pp 处于同一量级,说明「合并会显著损伤通用能力」的说法在本规模下不成立。
- 第三次 SFT 并不必然更强:本实验里它在工具调用上反而低于合并结果,说明「混数据重训」同样会吃掉已有能力,只是方向不同。
工程结论:先用 8 分钟试合并,不通过再上训练。这是一条严格占优的决策顺序——合并的失败成本是分钟级,重训的失败成本是天级。
总结与未来展望
- 模型合并的本质是任务向量的算术:微调是叠加在 base 上的结构化扰动,只要父模型同架构、同 tokenizer、同 base,就可以在权重空间直接相加,无需任何训练数据。
- 两个模型用 SLERP(保范数、防变钝),三个以上用 DARE-TIES(剪冗余 + 解符号冲突);
density管剪多狠、weight管谁更重要、t的分层渐变是最好用的旋钮。 - 真正的风险不在算法,而在流程:tokenizer/chat_template 错位、fp16 NaN、量化权重不可合并、安全对齐被平均掉、只看聚合分数掩盖单能力回退——这五条是生产事故的绝大多数来源。
- 未来的方向已经清晰:层感知与无监督对齐的合并(ICLR 2026 Expert Merging)、针对 RL 稀疏增量的专用方法(RAM)、单 LoRA 持续学习式的「合并后再忘」(SLAO)、以及把合并做成分布式可收敛操作(CRDT 合并)。趋势是「合并从手工配方走向可搜索、可验证、可流水线化的工程能力」。
- 如果你的团队还在用「再跑一次 SFT」解决多能力问题,那么本文最值钱的一句话是:先花 8 分钟 merge 一次,再决定要不要花 8 小时训练。
延伸阅读
标签:模型合并 · Model Merging · TIES · DARE · mergekit · LoRA · 灾难性遗忘 · 任务向量 · 微调工程化 · 生产部署
参考与数据来源:mergekit 官方仓库与 merge method 文档(arcee-ai/mergekit)、mergekit 论文(arXiv 2403.13257)、TIES-Merging(arXiv 2306.01708)、DARE(arXiv 2311.03099)、Model Soups(arXiv 2203.05482)、Task Arithmetic(arXiv 2212.04089)、Model Merging and Safety Alignment(EMNLP 2024 Findings)、PEFT 模型合并文档、HuggingFace「Merge LLMs with mergekit」教程、生产环境模型合并实践指南(tianpan.co, 2026-04)、SLERP vs 第三次微调的成本对比(Spheron, 2026-09)。