登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

PEFT 适配器合并后为什么输出会变化

来源:17golang原创

时间:2026-09-28 17:52:47 221浏览 收藏

PEFT 适配器合并后输出发生变化,不一定说明合并失败。对 LoRA 这类加性方法,merge_and_unload() 会把适配器增量写入基础权重;但“前向时分开计算再相加”和“先把权重相加再做前向”走的是不同浮点运算路径,低精度下可能产生细小 logits 差异。若文本变化很大,通常应继续检查基础模型 revision、dtype/量化、活跃适配器、tokenizer 和生成参数。

PEFT 官方文档:https://huggingface.co/docs/peft/

排查顺序
  • 先比较 logits,不要先比较带采样的生成文本。
  • 确认合并前后使用同一基础模型、同一适配器、同一 dtype 和同一输入。
  • 正确接收 merge_and_unload() 的返回模型;官方文档明确说明它不是原地返回语义。
  • safe_merge=True 用于检查适配器权重中的潜在 NaN,不等于输出一致性证明。

合并前后数学等价,但数值路径不完全相同

以 LoRA 为例,推理时可以把基础层输出和低秩适配器输出分别计算后相加,也可以先形成 W' = W + ΔW 再使用普通线性层。实数数学里两者等价;在 FP16、BF16 或其他有限精度下,矩阵乘、缩放和加法的舍入位置不同,最终 logits 可能出现小差异。

我第一次遇到“合并后回答变了”时,直接盯着最终文本看,结果把采样放大的差异误判成权重错误。后来改成固定输入、关闭采样、比较 logits,才发现模型排序只在非常接近的候选 token 上发生了交换。

PEFT LoRA 基础权重、适配器增量和合并权重关系结构说明图
图1:结构说明图,展示基础权重 W、LoRA 增量 ΔW 与 merge_and_unload 后合并权重 W' 的静态关系。

先固定模型身份和推理条件

比较前应把会改变输出的条件逐项锁定。基础模型必须是同一个仓库和 revision;适配器也要来自同一 checkpoint。加载时显式指定 torch_dtype,并让合并前后的模型都处于 eval()。tokenizer、聊天模板、输入文本、padding、截断和 generation config 也必须一致。

如果代码先加载了多个适配器,要确认当前 active adapter 与准备合并的 adapter_names 一致。PEFT 文档说明:未指定 adapter_names 时会合并活跃适配器;加载了适配器并不意味着它自动成为活跃适配器。

用同一输入比较 logits,而不是先看生成文本

下面的最小示例先取得未合并模型的 logits,再接收合并模型并对同一输入计算一次。示例不假定某个固定阈值适用于所有模型;请根据 dtype、模型规模和任务定义容差。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

base_id = "your-org/base-model"
adapter_id = "your-org/lora-adapter"
revision = "fixed-commit-or-tag"

# 基础模型、分词器和适配器都固定到同一版本来源。
tokenizer = AutoTokenizer.from_pretrained(base_id, revision=revision)
base_model = AutoModelForCausalLM.from_pretrained(
    base_id,
    revision=revision,
    torch_dtype=torch.float32,
    device_map="cpu",
)
peft_model = PeftModel.from_pretrained(base_model, adapter_id)
peft_model.eval()

# 使用完全相同的 token 输入,避开聊天模板和采样差异。
inputs = tokenizer("比较合并前后的输出", return_tensors="pt")
with torch.inference_mode():
    logits_before = peft_model(**inputs).logits.float().cpu()

# merge_and_unload 会返回基础模型形态,必须接收返回值继续使用。
merged_model = peft_model.merge_and_unload(safe_merge=True)
merged_model.eval()
with torch.inference_mode():
    logits_after = merged_model(**inputs).logits.float().cpu()

# 同时查看最大绝对误差和相对容差判断,阈值需按项目校准。
max_abs_diff = (logits_before - logits_after).abs().max().item()
is_close = torch.allclose(
    logits_before,
    logits_after,
    atol=1e-5,
    rtol=1e-4,
)
print({"max_abs_diff": max_abs_diff, "is_close": is_close})

这里用 FP32 和 CPU 是为了缩小变量,不代表生产部署必须如此。若 FP32 下差异很小,而换成 BF16、FP16 或量化后明显增大,问题更可能在数值精度和量化路径,而不是适配器 checkpoint 本身。

merge_and_unload 最容易用错的两个地方

第一,必须使用返回值:merged_model = peft_model.merge_and_unload()。官方 tuners 文档明确提醒,该操作返回与原始基础模型相同架构的模型,不应假设原变量仍保留完整的 PEFT 接口语义。

第二,合并后适配器模块被卸载,得到的是独立基础模型形态。此后不能再依赖 set_adapter() 动态切换,也不能把 unload() 和 merge_and_unload() 混为一谈:前者移除适配器而不合并,后者先把权重增量写入基础模型再卸载。

差异变大时先检查四个边界

检查项常见偏差核对方式
基础模型身份adapter 训练时的 base 与合并时 revision 不同固定仓库、commit/tag 和模型配置
适配器选择加载了多个 adapter,却合并了错误的 active adapter打印 active adapters,并显式传入 adapter_names
数值精度合并前后 dtype 不同,或量化权重经历反量化/再量化先用 FP32 做基线,再逐步加入低精度
推理配置tokenizer、模板、随机种子、采样参数不同先比较固定 tokens 的 logits,再比较 greedy 生成
PEFT 合并输出差异中的模型身份、数值精度和推理配置边界结构说明图
图2:结构说明图,展示基础模型 revision、活跃 adapter、dtype、量化、tokenizer 和 generation_config 对输出 logits 的静态影响关系。

量化尤其容易让问题变复杂。PEFT 的量化指南强调,量化表示本身会用更少位数近似权重;不同量化后端对 merge/unmerge 的支持也不完全一致。我的取舍是:先在未量化的可合并 dtype 下生成并保存 merged checkpoint,再按目标推理后端执行一次明确的量化流程,而不是默认“量化模型上直接合并”一定保持相同结果。

生成文本变化不代表 logits 偏差很大

当 do_sample=True 时,一个很小的概率变化也可能让首个采样 token 不同,后续文本便完全分叉。即使关闭采样,两个候选 token 的 logits 极其接近时,细小舍入也可能改变排序。因此建议按三层验收:

  1. 比较同一 tokens 输入下的 logits 误差。
  2. 使用 do_sample=False 比较 greedy 输出。
  3. 最后才在固定随机种子、相同温度和 top-p 下比较采样结果。

如果 logits 已在项目容差内,而业务指标也没有显著下降,文本存在少量差异通常可以接受;如果 logits 大幅偏离,就回到前一节逐项排除身份、适配器、精度和配置问题。

什么时候不应该合并

如果服务需要在多个 adapter 之间动态切换、按用户组合 adapter,或继续训练适配器,就保留 PEFT 模型结构。合并更适合单一 adapter 的固定推理部署,希望使用普通基础模型架构、减少适配器分支开销或导出独立 checkpoint 的场景。

另外,并非所有 PEFT 方法和底层层类型都支持相同的 merge 行为。遇到不支持的 tuner 或量化层,应以当前官方文档和实际 API 报错为准,不要强行转换权重。

相关问题

safe_merge=True 能保证输出完全一致吗?

不能。它用于在合并时检查适配器权重中潜在的 NaN,不负责证明合并前后 logits 或生成文本逐位相同。

合并后还能继续切换 adapter 吗?

不能按原来的 PEFT 方式切换。merge_and_unload 返回基础模型形态,适配器层已经卸载;需要动态切换时应保留未合并模型。

为什么同一个 prompt 每次生成都不一样?

先检查是否启用了采样。固定模型还不够,还要固定 tokenizer 输入、随机种子、temperature、top-p、max_new_tokens 等生成参数;排查合并时应先关闭采样比较 logits。

多个 LoRA 可以一次合并吗?

部分兼容场景支持多个活跃 adapter,但要显式确认组合方式、加载顺序和 adapter_names。混合 adapter 类型存在兼容限制,不能默认任意组合都可保存或加载。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>