一句话

训练脚本保存的是 LoRA adapter(一个几十 MB 的增量),不能直接用 AutoModelForCausalLM.from_pretrained 加载;必须先加载基座,再接 adapter,最后合并。

触发条件

用 PEFT / TRL 做 LoRA 微调,训练结束后 trainer.save_model 产出目录通常长这样:

runs/sft_06b_label/
  adapter_config.json         # 关键:有这个文件就说明是 adapter,不是完整模型
  adapter_model.safetensors   # 只有 LoRA 增量,例如 38 MB
  tokenizer.json
  chat_template.jinja
  training_args.bin

然后写评测脚本时顺手写成:

model = AutoModelForCausalLM.from_pretrained("runs/sft_06b_label")

症状

两种表现,第二种更麻烦:

  1. 直接抛异常,说缺 config.json 里的 model_type 或权重不匹配;
  2. 不报错但结果完全不对——加载出一个只有 adapter 键、主体权重随机初始化的模型。生成的是乱码或固定重复的 token,而评测脚本一路正常跑完,指标难看但看起来像"模型没学好"。

第 2 种是最难查的一类 bug:没有异常、没有日志,只有一个假的坏结果。

根因

adapter 目录里没有基座模型权重。AutoModelForCausalLM 期望的是完整 checkpoint,而 adapter_config.json 里的 base_model_name_or_path 才是基座位置。PEFT 用适配器注入的方式改写注意力层与 MLP 层的线性层,这个改写必须由 PeftModel 完成。

正确做法

import json
from pathlib import Path
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

adapter_cfg = Path(model_path) / "adapter_config.json"
tok = AutoTokenizer.from_pretrained(model_path)
if adapter_cfg.exists():
    base = json.loads(adapter_cfg.read_text(encoding="utf-8"))["base_model_name_or_path"]
    model = AutoModelForCausalLM.from_pretrained(base, dtype=torch.float32)
    model = PeftModel.from_pretrained(model, model_path)
    model = model.merge_and_unload()   # 把增量合并进基座
else:
    model = AutoModelForCausalLM.from_pretrained(model_path, dtype=torch.float32)
model.eval()

要点:

  • 用 adapter_config.json 是否存在来判定,这是唯一可靠的信号,不要靠目录名猜
  • base_model_name_or_path 可能是相对路径(训练时传的是相对路径),所以评测脚本要在项目根目录下运行,或者把它解析成绝对路径
  • merge_and_unload 把 LoRA 权重合并进基座,推理更快,也不再需要 peft 参与前向;若还要继续训练则不要 merge
  • tokenizer 可以直接从 adapter 目录读,因为训练脚本把 tokenizer 也存进去了

怎么下次一眼认出来

  • 看到模型目录里有 adapter_config.json,就绝对不能用裸 from_pretrained
  • 保存出来的权重只有几十 MB(而基座是几百 MB 到几 GB),几乎必然是 adapter
  • 评测结果异常差但没有任何报错时,先确认模型到底加载的是什么,而不是先去怀疑数据

相关


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/LLM