【Bug已解决】Support Original Checkpoint-Compatible PEFT Adapters 解决方案

📅 2026/8/11 1:29:08
【Bug已解决】Support Original Checkpoint-Compatible PEFT Adapters 解决方案
【Bug已解决】Support Original Checkpoint-Compatible PEFT Adapters 解决方案一、现象长什么样diffusers 通过LoraLoaderMixin支持 PEFT 风格的 LoRA但很多用户是从原始训练框架如 Kohya_ss、基于peft库直接save_pretrained的脚本、或自研训练器导出的 adapter checkpoint文件名和 key 布局和 diffusers 期望的不一致。加载时报from diffusers import StableDiffusionXLPipeline pipe StableDiffusionXLPipeline.from_pretrained(stabilityai/sdxl-base-1.0) pipe.load_lora_weights(my_training_output/adapter_model.safetensors) # 原始 peft 导出报错KeyError Cannot find lora_unet_up_blocks_0_attentions_0_to_q.lora_up.weight in checkpoint或者文件结构不对FileNotFoundError adapter_config.json not found in checkpoint directory又或者文件名是pytorch_lora_weights.safetensors而非 diffusers 约定的pytorch_lora_weights.bin 特定 key 前缀导致load_lora_weights找不到预期入口。现象总结原始训练框架导出的 PEFT adapter checkpoint 的 key 命名、文件结构config 名、权重文件名与 diffusersload_lora_weights的约定不兼容直接加载会 KeyError / FileNotFoundError。二、背景PEFTParameter-Efficient Fine-Tuning库保存 adapter 时约定目录里有adapter_config.json描述r、lora_alpha、target_modules等权重文件名adapter_model.safetensors/adapter_model.binkey 形如base_model.model.unet.up_blocks.0.attentions.0.transformer_blocks.0.attn1.to_q.lora_A.default.weight带base_model.model.前缀和lora_A/lora_B命名。而 diffusers 的load_lora_weights期望权重文件名pytorch_lora_weights.safetensors或.binkey 不带base_model.model.前缀且用lora_up/lora_down命名to_q.lora_up.weight不强制要求adapter_config.json而是用 pipeline 自身结构推断。两套约定在「前缀、命名lora_A/B vs lora_up/down、文件名」三处都不同于是原始 checkpoint 无法直接被 diffusers 吃进去。三、根因根因三点key 前缀与命名不一致PEFT 用base_model.model....lora_A.default.weightdiffusers 用...to_q.lora_down.weight没有映射层。文件名约定不一致PEFT 用adapter_model.*diffusers 默认找pytorch_lora_weights.*不认前者。缺少 config 兼容读取diffusers 不读adapter_config.json但原始 checkpoint 的r/alpha信息在那里缺失时缩放可能按默认算错。本质diffusers 只认自己的 LoRA 格式没有一层把「原始 PEFT checkpoint」翻译成 diffusers 格式去前缀、改名、补 config的兼容适配器。四、最小可运行复现用标准库复现「PEFT key 前缀导致 diffusers 找不到」# PEFT 导出的 key带前缀 lora_A/B PEFT_KEYS { base_model.model.unet.up_blocks.0.attentions.0.to_q.lora_A.default.weight: None, base_model.model.unet.up_blocks.0.attentions.0.to_q.lora_B.default.weight: None, } # diffusers 期望的 key DIFFUSERS_EXPECT unet.up_blocks.0.attentions.0.to_q.lora_up.weight def find_in_peft(peft_sd, diffusers_key): # 尝试匹配去掉前缀lora_A-lora_down, lora_B-lora_up target diffusers_key.replace(.lora_up.weight, .lora_B.default.weight) \ .replace(.lora_down.weight, .lora_A.default.weight) for k in peft_sd: if k.endswith(target): return k raise KeyError(fCannot find {diffusers_key} in checkpoint) try: find_in_peft(PEFT_KEYS, DIFFUSERS_EXPECT) except KeyError as e: print(KeyError, e) # 因为没去掉 base_model.model. 前缀要复现成功把匹配逻辑改成「先k.replace(base_model.model., )再比」即可命中。五、解决方案第一层最小直接修复最小修复写一个「原始 PEFT checkpoint → diffusers 格式」的兼容转换函数去前缀、改命名、统一文件名import re def peft_to_diffusers_key(peft_key: str) - str: # 1) 去 base_model.model. 前缀 k peft_key.replace(base_model.model., ) # 2) lora_A - lora_down, lora_B - lora_up并去掉 .default k k.replace(.lora_A.default.weight, .lora_down.weight) k k.replace(.lora_B.default.weight, .lora_up.weight) # 3) peft 的 unet/transformer 前缀映射到 diffusers 的 unet/transformer 路径 return k def convert_peft_checkpoint_to_diffusers(peft_sd: dict) - dict: out {} for key, val in peft_sd.items(): dk peft_to_diffusers_key(key) out[dk] val return out同时加载时允许指定文件名pipe.load_lora_weights(my_training_output, weight_nameadapter_model.safetensors)让 diffusers 能认 PEFT 的文件名。六、解决方案第二层结构性改进把「原始 PEFT checkpoint 与 diffusers 的格式差异」收敛成一个 dataclass 单一真源转换只依赖它from dataclasses import dataclass, field from typing import Dict dataclass(frozenTrue) class PeftCompatPolicy: 原始 PEFT checkpoint 兼容 diffusers 的单一真源。 # PEFT key 前缀需去除 strip_prefixes: tuple (base_model.model.,) # PEFT 命名 - diffusers 命名 name_map: Dict[str, str] field(default_factorylambda: { .lora_A.default.weight: .lora_down.weight, .lora_B.default.weight: .lora_up.weight, }) # PEFT 文件名 - diffusers 约定文件名 file_aliases: Dict[str, str] field(default_factorylambda: { adapter_model.safetensors: pytorch_lora_weights.safetensors, adapter_model.bin: pytorch_lora_weights.bin, }) # config 文件名 config_name: str adapter_config.json # config 里需读取的字段 required_config_fields: tuple (r, lora_alpha, target_modules) def translate_key(self, peft_key: str) - str: k peft_key for p in self.strip_prefixes: k k.replace(p, ) for src, dst in self.name_map.items(): k k.replace(src, dst) return k def resolve_weight_name(self, filename: str) - str: return self.file_aliases.get(filename, filename) def read_alpha(self, config: dict) - float: r config[r] alpha config.get(lora_alpha, r) return alpha / r # diffusers 用的缩放转换主函数load_original_peft_adapter(pipe, path, policy)只调用policy.translate_key/resolve_weight_name/read_alpha新增任何 PEFT 变体只需在name_map/file_aliases补充。七、解决方案第三层断言 / CI 守护用 pytest 把「前缀去除 命名转换 alpha 计算 文件名识别」固化成回归import pytest from mylib.peft_compat import PeftCompatPolicy, convert_peft_checkpoint_to_diffusers POLICY PeftCompatPolicy() def test_strip_prefix_and_rename(): peft_key base_model.model.unet.up_blocks.0.attentions.0.to_q.lora_A.default.weight dk POLICY.translate_key(peft_key) assert dk unet.up_blocks.0.attentions.0.to_q.lora_down.weight def test_lora_b_maps_to_up(): peft_key base_model.model.unet.up_blocks.0.attentions.0.to_q.lora_B.default.weight assert POLICY.translate_key(peft_key).endswith(to_q.lora_up.weight) def test_weight_name_alias(): assert POLICY.resolve_weight_name(adapter_model.safetensors) \ pytorch_lora_weights.safetensors def test_alpha_scaling(): config {r: 8, lora_alpha: 16, target_modules: [to_q, to_v]} assert POLICY.read_alpha(config) 2.0 def test_full_conversion_preserves_count(): peft_sd { base_model.model.unet.up_blocks.0.attentions.0.to_q.lora_A.default.weight: None, base_model.model.unet.up_blocks.0.attentions.0.to_q.lora_B.default.weight: None, } converted convert_peft_checkpoint_to_diffusers(peft_sd) assert len(converted) 2 assert all(k.startswith(unet.) for k in converted) assert any(k.endswith(lora_up.weight) for k in converted)CI 把test_strip_prefix_and_rename与test_alpha_scaling作为 PEFT 兼容层的必过项要求「任何 PEFT 导出格式变更都要更新PeftCompatPolicy并重跑」。八、排查清单原始 PEFT adapter 加载失败按顺序查报KeyError找不到lora_up/down检查 PEFT key 是否带了base_model.model.前缀用translate_key去除。报FileNotFoundError: adapter_config.jsondiffusers 不强制要它但若报错是因为你显式传了config路径用weight_name指定真实文件名即可。文件名是adapter_model.safetensors用load_lora_weights(path, weight_nameadapter_model.safetensors)认它。缩放比例对不对diffusers 用alpha/rPEFT config 里有缺失会用默认r导致比例错、效果弱。lora_A是否映射成lora_down、lora_B成lora_up映射反了权重方向错。target_modules是否覆盖 checkpoint 里所有模块PEFT 可能训了ff.net等非注意力层diffusers 侧目标模块要对应得上。九、小结「Support Original Checkpoint-Compatible PEFT Adapters」本质是diffusers 只认自己的 LoRA 格式没有一层把原始 PEFT checkpoint前缀、命名、文件名、config翻译成 diffusers 格式的兼容适配器导致 KeyError / FileNotFoundError / 缩放错。第一层写peft_to_diffusers_key去前缀改名 允许指定weight_name第二层把格式差异收敛到PeftCompatPolicy单一真源转换只依赖它第三层用 pytest 守住「前缀去除、命名转换、alpha 计算、文件名识别」。通用教训**支持外部生态的产物时必须在边界处提供「格式兼容层」把对方的 schema 翻译成自己的并把映射规则列为单一真源否则每换一个导出工具就 KeyError 一片。