模型合并这个事儿圈子里聊得越来越多。你手头有微调过的LoRA、有几个基础模型但每次任务都要来回切换权重推理时间翻倍显存也吃紧。04.02.07这个编号在我熟悉的AI工具链里对应的是Model Merge Tool也就是模型合并工具它解决的不只是“把文件拼一起”这种表面的需求而是把多个模型的能力压缩进一份权重里让模型既保留A任务的风格又兼顾B任务的知识同时推理时只需要加载一次。这篇内容适合正在做模型微调、想减少部署成本、或者想实验多模型能力融合的同学我会从原理讲到实操再聊聊踩过的坑。1. 为什么需要模型合并从模型微调到多模型融合1.1 模型合并解决什么问题先说说场景。前阵子我在做一个中文对话模型底模用的是某个开源基座为了让它听话我先用指令数据微调了一版后来又要它写代码又用代码语料微调了一版。可问题是这两版模型不兼容指令能力强的代码能力弱代码强的又变得不爱搭理人。重新找数据混合训练成本高而且容易把原有能力冲掉。这时候模型合并就派上用场了把两三个微调后的模型按一定策略合并成一份新权重。合并后的模型同时具备两种能力推理时一次加载显存占用和延迟都没有额外增加。这种技术并不是玄学。大模型的权重里不同的能力往往分布在不同层、不同参数上。微调过程中大部分参数其实变化很小只有少部分参数记住了特定任务的特征。合并的实质就是对“哪些参数值得保留、哪些可以折中”做一次重新分配。用个生活类比你有一个会做川菜的厨师还有一个会做粤菜的厨师模型合并不是让他俩同时掌勺而是把火候、刀工、调味心得融合进同一个人身上这个“人”就是合并后的权重。1.2 合并与模型集成、微调的关系很多人会混淆模型合并和模型集成。集成是同时运行多个模型投票或加权得到结果推理开销直接翻倍合并则是离线把模型权重计算成一份新的权重运行时完全无感。两者思路完全不同集成是“人多力量大”合并是“取长补短”。所以合并特别适合那些限制推理资源的生产环境。另外合并和微调也不是互斥关系。你可以先合并再继续微调也可以先微调再合并。实际操作中我更常用的是“基座 多个微调分支”的合并方式基座保持不动微调分支只保留差异参数合并时把差异重新注入基座。这样做的好处是稳定基座的原始能力不会因为多个微调分支之间的相互干扰而崩溃。这也是后面要讲的TIES合并法的核心思想。2. 主流模型合并方法与底层原理2.1 权重平均与SLERP球面线性插值最简单的合并方法就是把权重直接取平均。比如有两个模型把每一层的权重张量对应元素相加除以2。这个方法在任务相近、模型参数分布接近的时候效果还行但如果两个模型差异很大平均出来的模型往往两头不讨好。原因很简单大模型的权重空间是高维非线性的参数方向的平均值不代表能力方向的中间态可能把某个维度上的强特征直接对冲掉了。更讲究一点的是SLERP球面线性插值。它不用欧氏距离的直线平均而是把权重向量视为高维球面上的点按照球面上的最短路径测地线做插值。好处是合并过程中能保持权重的方向和范数特征不会因为简单的加减而破坏原有分布。SLERP在LoRA合并、基础模型融合里的口碑普遍比直接平均好。你可以把它理解为导航走的是球面大圆航线而不是平面直线前者更贴近地球表面的真实路径。2.2 TIES合并与DARE剪枝TIES的全称是Trim Elect Sign and Merge三步走压缩、符号统一、合并。先看多个模型在同一位置上的权重差异把差异小的参数直接剪掉只保留差异大的然后对保留的差异按照多数模型的符号方向做统一避免正负相互抵消最后才做加权合并。这套机制解决的核心问题就是“冲突”多个模型在同一参数上想要的方向不一致其中某个模型可能只是过拟合了少部分样本但它的方向会拖累整体。TIES把这种噪声过滤掉让合并结果更稳定。DARE则是从“剪枝”入手的。它发现LoRA微调后的模型绝大部分参数其实没有变化真正有效的稀疏差异。DARE随机丢弃一部分微调增量delta然后按比例放大保留下来的增量再合并到基座上。这样做居然能几乎不损失性能说明模型的多任务能力存在很大的冗余。在实际合并中DARE常和TIES结合使用先DARE剪枝减噪再TIES统一方向最后加权融合效果比单独使用好很多。2.3 分层合并与参数选择除了整体的合并算法还有一个容易被忽视的维度逐层合并。模型的不同层承担不同的职责比如底层偏向通用语法特征中层偏向语义理解高层偏向具体任务输出。所以在合并时不一定所有的层都采用相同的合并策略。你可以选择前10层用SLERP中间层用TIES最后10层用直接平均甚至可以对每层单独计算一个最佳插值系数。参数选择层面的另一个关键点是“什么可以合并”。合并的对象不只是完整的检查点checkpoint还包括LoRA适配器、Delta权重和EMA影子权重。在实际工程里最稳定的合并单位其实是“基座 差分权重”。先把微调模型和基座的权重相减得到delta权重然后对delta做合并最后加回基座。这样合并算法只处理差异部分数值范围更小不容易因为插值两个绝对值很大的权重而引入浮点误差。3. 实操用Model Merge Tool完成一次模型合并3.1 环境准备与工具安装Model Merge Tool这个名字在开源社区里有对应的实现通常在Hugging Face的transformers基础上使用。最常用的库是mergekit它整合了SLERP、TIES、DARE等多种合并方法而且支持GPU加速。安装很简单pip install mergekit如果要用到TIES和DARE还需要安装torch和transformers建议直接用虚拟环境避免污染系统Python。我自己习惯用conda创建环境conda create -n merge python3.10 -y conda activate merge pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install mergekit transformers accelerate注意版本匹配Python 3.10配PyTorch 2.x基本稳。如果显存紧张合并也可以用CPU但速度会慢到怀疑人生。我建议至少16GB显存否则合并7B以上的模型非常痛苦。3.2 合并配置与参数设置mergekit通过YAML配置文件描述合并任务。先看一个实验配置我要把“代码能力强”的模型和“对话能力强”的模型合并到一个基座上slices: - sources: - model: base-model layer_range: [0, 16] - model: code-model layer_range: [0, 16] - model: chat-model layer_range: [0, 16] merge_method: slerp parameters: t: - filter: self_attn value: [0.5, 0.5, 0.6, 0.6, 0.7, 0.7] - filter: mlp value: [0.3, 0.3, 0.4, 0.4, 0.5, 0.5] - filter: layernorm value: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]这里解释几个关键点。layer_range指定了参与合并的层范围我选择前16层。t是插值系数0.5表示两个模型对半权重越接近1表示更偏向第一个来源这里是base-model。为什么按层设置不同t值因为我测试发现底层更通用不宜大幅偏离基座所以设置layernorm层的t为1.0而注意力层和MLP层保留微调模型的较多特征。这种细节是文档里不常写的我花了几个晚上对比才找到合理规律。如果使用TIES方法配置会更复杂一点merge_method: ties base_model: base-model slices: - sources: - model: code-model layer_range: [0, 32] - model: chat-model layer_range: [0, 32] - model: code-model layer_range: [0, 32] parameters: normalize: true int8_mask: true weight_merge_style: linear consensus_method: sumties方法必须有base_model字段因为TIES要计算每个模型相对基座的delta再做符号统一。int8_mask表示用int8精度保存mask能大幅减少内存占用但精度会有微小损失。我实测下来影响不大内存吃紧时建议开启。3.3 运行合并并测试效果写好配置文件后执行命令mergekit-yaml merge_config.yaml merged-model --cuda --copy-tokenizer如果一切正常会看到进度条和每层的合并日志。这里有个小坑copy-tokenizer会复制第一个来源模型的tokenizer如果两个模型词表不一致合并后输出乱码。务必确认参与合并的模型词表一致或者手动指定一个兼容的tokenizer。我的做法是先做tokenizer对齐把两个模型的tokenizer加载后比较词表大小不一致则先扩容到统一词表再合并。合并完成后不能直接拿高并发服务去压测先做基础验证。第一步加载模型并问几个不同任务的问题第二步跑一遍原本各自擅长任务的评测集比如代码模型用HumanEval对话模型用AlpacaEval第三步查看困惑度perplexity。我通常先看困惑度比如用mergekit合并后在验证集上困惑度从12.4左右涨到13.1这个幅度是正常的说明能力略有损失但仍在可接受范围如果涨到15以上基本可以判定合并策略不合理需要调整t值或层范围。4. 常见问题与排查实录4.1 合并后模型退化或灾难性遗忘这是最普遍的问题。合并前感觉万事俱备合并后一测试发现代码能力提升了但对话变得僵硬或者两个任务都下降那大概率是合并时没有给基座留足“底线”。我常用的排查思路是先单独合并每个微调模型与基座看看是否稳定如果单模型合并都会退化说明微调分支本身的delta噪声太大需要先用DARE剪枝如果单模型合并正常多模型合并才退化那么重点检查TIES的参数尤其是consensus_method改成majority或者sum观察差异。另一个有效技巧是“排除层合并”。有些模型只在最后几层保存任务特征如果合并时把最后几层也平均化灾难性遗忘就容易出现。可以试试前80%的层用slerp最后20%的层完全采用其中一个模型的值。这有点像是“谁擅长什么就让谁做主”效果往往立竿见影。4.2 内存不足与OOM处理合并过程中OOM几乎是必然体验。第一次合并13B模型时我原本以为32GB内存够用结果在加载多个checkpoint时直接爆掉。解决办法有几个按推荐顺序排使用float16加载权重而不是默认的float32。借助mergekit的low_cpu_mem参数它会流式加载权重只保留必要的张量。按层合并分片保存不要一次性把所有层的权重都放进内存。启用int8_mask把mask压缩成int8。如果你用的是SLERP还需要注意t值的float64精度numpy默认会用float64造中间张量对内存压力很大。可以在配置参数里把dtype设置为bfloat16或float16显存占用能降一半。4.3 合并权重选择与模型兼容性参与合并的模型必须满足几个前提同样的架构、同样的词表、同样的参数量、同样的训练目标。比如一个模型是GPT-like结构一个是Encoder-Decoder结构这两者没法直接合并因为层维度都对不上。即使同为LLaMA架构如果一个是基于原始LLaMA训练的一个是从Alpaca增量微调的它们的权重分布可能已经产生偏移合并前最好做权重的均值方差检查。如果两个模型的embedding输出范围差了一个数量级合并结果基本没法用需要先对delta做标准化。关于选择合并哪些来源我有个经验优先合并的是基座一致、任务互补的模型而不是两个都是重度微调、任务重叠的模型。任务重叠容易导致某个任务方向被放大另一个方向被淹没。我会先查看每个微调模型相对基座的delta幅值分布如果两个模型的delta高度重合合并没有太大意义如果delta分布互补合并收益就很高。4.4 效果验证与回归测试合并完成不是终点验证才是真正花时间的部分。我建议搭建一个轻量回归测试集包含三类数据第一类是通用语料上的困惑度用于检测整体退化第二类是目标任务的能力测试例如代码生成、指令遵循第三类是安全与格式化测试比如拒绝回答不当内容、输出JSON合法性等。这类回归测试每次合并前都要跑一遍记录基线和合并后的分数形成一个小的看板。你会发现即使固定合并方法调整t值也像在走钢丝少调0.05可能问题不大多调0.1可能直接崩。另外一个小技巧合并后不要急着做量化例如GPTQ或AWQ。量化是另一个误差来源如果合并和量化同时做出问题时很难定位是哪个环节导致的。先把合并后的浮点模型验证满意再做量化再重新验证一遍。我曾试过合并后直接量化结果模型输出开始复读和乱码排查了两天才发现是量化校准数据没覆盖率新合成模型的任务分布。我个人最深的体会是模型合并从来不是“一键拼接”它更像是在做权重空间的调酒。每种合并方法是一种基酒t值是比例层范围是摇晃的手法。没有放之四海皆准的参数只有对模型底层的理解和对任务的清醒认知。最后再分享一个务实的小技巧在最终确定合并方案前可以先把参与合并的模型统一以LoRA形式微调让它们的权重分布更加接近基座然后再执行合并。这样delta量级更小合并后的模型泛化能力往往优于直接合并全量微调模型。当然这会让训练时间变长收益是否值得需要根据项目周期权衡但在我做过的多次合并中这个策略的稳定性几乎是最好的。