把两个LoRA权重直接相加后加载到同一个基座模型,效果经常不升反降。参数空间的方向冲突会让模型在生成时同时触发两种风格,造成语义漂移甚至事实错误。要解决这个问题,得先理解LoRA增量矩阵在合并时发生的数学层面的干扰,然后再选择合适的融合与算术策略。

一、LoRA权重冲突为什么不能靠简单相加解决
LoRA的微调过程并不会修改原始权重,而是学习两个低秩矩阵A和B,最终推理时把W替换为W + BA。假设任务一得到增量ΔW1 = B1A1,任务二得到增量ΔW2 = B2A2,最直观的合并方式就是W + ΔW1 + ΔW2。但这个表达式隐含了一个强假设:两个增量在参数空间中方向一致,或者至少不冲突。实际上不同任务的梯度方向经常正交甚至相反。
可以用余弦相似度观察冲突程度。把两个增量矩阵展平成向量后计算内积,如果相似度接近-1,说明一个任务希望某层权重增大,另一个任务却希望它减小。直接相加后,公共维度上的信息被抵消,而某些维度又因为重复叠加而放大,导致模型既丢失了单任务能力,又产生不稳定的输出。更麻烦的是,LoRA的低秩结构会放大这种冲突。因为BA的秩很低,所有更新都集中在少数奇异方向上,一旦两个任务的更新方向接近相反,合并后有效秩可能进一步下降。
对文本生成任务来说,这种冲突表现为:加载写作风格LoRA和代码能力LoRA后,模型既能写散文又能写代码,但散文里夹杂代码片段,代码注释又过于口语化。这不是权重比例没有调好,而是底层的增量方向发生了对抗。因此需要引入系统的融合方法,而不是调大或调小某个任务的系数就能解决。
import torch
def cosine_similarity(a, b):
a = a.float().flatten()
b = b.float().flatten()
return torch.dot(a, b) / (a.norm() * b.norm() + 1e-12)
base = torch.randn(4096, 4096)
lora_a = torch.randn(64, 4096) * 0.02
lora_b = torch.randn(4096, 64) * 0.02
delta1 = lora_b @ lora_a
lora_c = torch.randn(64, 4096) * 0.02
lora_d = torch.randn(4096, 64) * 0.02
delta2 = lora_d @ lora_c
sim = cosine_similarity(delta1, delta2)
print("增量相似度:", sim.item())
# 如果 sim 接近 -1,说明两个任务在相反方向更新,
# 此时简单相加会明显抵消单任务能力。
二、适配器融合:门控、加权与正交化
适配器融合的思路不是重新训练模型,而是给不同任务的增量矩阵分配更合理的组合方式。最简单的是线性加权,即ΔW = α1ΔW1 + α2ΔW2。两个标量系数可以手动调整,也可以通过学习得到。手动加权适合任务数量少、验证集明确的场景,比如α1=0.7、α2=0.3分别对应主任务和辅助任务。但线性加权无法处理方向冲突,它只能控制每个增量的整体幅度,不能修正公共方向上的重叠或对抗。
更有用的是在融合前对增量矩阵做正交化。如果两个任务更新方向存在重叠,可以用Gram-Schmidt过程移除一个增量在另一个增量方向上的投影,使剩余部分只保留新的信息。这样加权融合时,公共方向由第一个任务主导,第二个任务只贡献新增量,避免重复叠加。不过正交化也有代价:它破坏了原始LoRA增量与训练目标之间的一致性,可能让单任务能力略有下降。因此更稳健的实践是先计算相似度,只有相似度绝对值较高时才做正交化,否则直接加权即可。
门控融合是另一种适配器级方案。它的思路是给每层或每个模块学习一个门控向量,让模型根据输入动态选择需要哪些任务知识。这通常需要额外的训练数据或蒸馏过程,工程成本更高,但能明显缓解多任务之间的干扰。对LoRA来说,门控可以作用在ΔW的缩放系数上,也可以作用在A和B的中间表示上。无论哪种实现,目标都是从静态权重混合走向输入感知的动态路由。
import torch
def weighted_fuse(base, deltas, weights):
fused = base.clone()
for w, delta in zip(weights, deltas):
fused += w * delta
return fused
def orthogonalize_two(delta1, delta2):
flat1 = delta1.float().flatten()
flat2 = delta2.float().flatten()
proj = torch.dot(flat2, flat1) / torch.dot(flat1, flat1) * flat1
flat2_orth = flat2 - proj
return delta1, flat2_orth.reshape_as(delta2)
d1 = torch.randn(256, 256) * 0.01
d2 = torch.randn(256, 256) * 0.01
d1_orth, d2_orth = orthogonalize_two(d1, d2)
fused_delta = 0.7 * d1_orth + 0.5 * d2_orth
三、任务向量算术:把LoRA增量当作可加减的向量
任务向量算术把微调前后的参数差看作一个语义向量,任务向量的加减就对应能力的组合与移除。以LoRA为例,τ1 = ΔW1、τ2 = ΔW2,合并模型可以写成θ_new = θ_base + λ(τ1 + τ2)。当任务冲突时,问题转化为如何从多个任务向量中提取共同的可靠方向。原生的Task Arithmetic假设所有任务向量可以线性相加,这在任务差异较小时有效,但冲突较大时性能会下降。
TIES方法针对冲突给出了三阶段处理。第一步裁剪掉每个任务向量中幅值较小的参数,只保留重要更新;第二步对剩余参数的符号做多数投票,决定每个位置最终应该增大还是减小;第三步只合并与多数符号一致的参数,忽略少数派更新。这样做的好处是:即使某个任务在少数参数上给出了相反方向,也不会污染最终结果。将TIES用于LoRA时,可以直接在每个增量矩阵上逐元素执行,不需要回到完整权重空间。
除了加法,任务向量还支持减法。例如从对话模型中减去有害内容对应的任务向量,可以在一定程度上降低特定行为。LoRA场景下,这种操作更轻量:保存一个负向任务的增量矩阵,并在推理时用θ_base + λ(τ_chat - τ_toxic)实现。需要注意的是,减法会引入新的方向,不一定能精确撤销训练时的非线性影响,因此在实际部署中通常只作为辅助手段,需要配合安全过滤。
import torch
def ties_merge(task_deltas, k=0.2):
trimmed = []
for delta in task_deltas:
flat = delta.flatten()
threshold = torch.quantile(flat.abs(), 1 - k)
mask = flat.abs() >= threshold
trimmed.append(torch.where(mask, flat, torch.zeros_like(flat)))
stacked = torch.stack(trimmed, dim=0)
sign_sum = stacked.sign().sum(dim=0)
majority_sign = torch.where(sign_sum > 0, 1.0,
torch.where(sign_sum < 0, -1.0, 0.0))
final = torch.zeros_like(stacked[0])
for vec in trimmed:
final += vec * (vec.sign() == majority_sign).float()
count = (stacked.sign() == majority_sign.unsqueeze(0)).float().sum(dim=0)
final = final / count.clamp(min=1)
return final.reshape_as(task_deltas[0])
d_a = torch.randn(4096) * 0.01
d_b = torch.randn(4096) * 0.01
d_c = torch.randn(4096) * 0.01
merged = ties_merge([d_a, d_b, d_c], k=0.3)
四、工程落地:如何选择融合策略与调参
实际项目里,选择哪种方案取决于任务数量、验证集规模和推理延迟要求。如果只有两个任务且冲突不严重,线性加权或者先正交化再加权是最省事的路线。任务数量达到三个以上时,TIES这类符号选举方法通常比简单平均更稳定,因为少数任务的异常方向会被投票机制抑制。适配器门控在任务数量多、输入分布变化大的场景下表现最好,但需要额外训练门控参数,部署复杂度也更高。
调参时,可以先固定一个主任务的权重为1,再以0.1为步长扫辅助任务系数。评价指标不要只看单个任务的生成质量,还要加入任务间串扰的测试。例如同时加载英文写作LoRA和中文古诗LoRA时,可以构造中英混合提示,看模型是否能在同一段文本中正确切换。另一个常见问题是合并后模型对提示格式变得敏感,原因是不同LoRA训练时使用了不同的提示模板。解决方式是在推理时统一模板,或者给不同任务配置不同的适配器开关,由路由决定调用哪一个。
最后还要注意,所有融合操作都应基于同一基底模型。如果两个LoRA分别基于不同版本或不同精度的底座训练,即使结构一致,参数空间的基线差异也会让融合结果不可控。合并前先确认底座的权重哈希,或者直接用同一份基底权重重新加载。对于需要频繁上线下线的多租户LoRA服务,建议把融合后的增量矩阵保存为独立文件,避免每次推理都重复计算。