Diffusers PEFT 适配器加载详解:PeftAdapterMixin 与 LoRA 集成实践
【免费下载链接】diffusers🤗 Diffusers: State-of-the-art diffusion models for image, video, and audio generation in PyTorch.项目地址: https://gitcode.com/GitHub_Trending/di/diffusers
本文以 🤗 Diffusers 仓库中的 PEFT API 文档 为核心,结合
src/diffusers/loaders/peft.py的源码实现,系统讲解 Diffusers 如何通过PeftAdapterMixin与 PEFT 库集成,在UNet2DConditionModel、SD3Transformer2DModel等模型类上加载、切换、缩放、融合与保存 LoRA 适配器。读完本文,你将掌握模型级适配器的完整生命周期管理,并能在实际推理与训练场景中直接落地使用。
一、背景:Diffusers 与 PEFT 的适配器生态
Diffusers 支持通过 PEFT 之类的适配器(adapter),其核心入口就是loaders.peft.PeftAdapterMixin这个混合类(mixin)。它让 Diffusers 中像UNet2DConditionModel、SD3Transformer2DModel这样的模型类可以直接与一个适配器协同工作,而不需要改动原有模型结构。
[!TIP] 有关 PEFT 在 Diffusers 中用于推理的整体使用概览,请参见仓库内的 Inference with PEFT 教程。本文聚焦于 API 层的
PeftAdapterMixin及其背后的源码实现。
PEFT(Parameter-Efficient Fine-Tuning)的核心思想是冻结原模型权重,只注入少量可训练参数。LoRA 通过在权重矩阵旁附加低秩分解矩阵lora_A @ lora_B实现这一点,使得一个适配器 checkpoint 通常只有几百 MB 甚至更小,加载轻量、易于分发。Diffusers 在模型层与 Pipeline 层提供了两套入口:
- Pipeline 层:
StableDiffusionLoraLoaderMixin/LoraBaseMixin,负责把 LoRA 同时分发到 UNet 和 text encoder; - 模型层:
PeftAdapterMixin,直接在单个模型(如 UNet 或 Transformer)上加载、管理适配器。
两者共享同一套 PEFT 后端与工具函数(见 src/diffusers/utils/peft_utils.py),本文重点剖析模型层的PeftAdapterMixin。
二、PeftAdapterMixin 类总览
PeftAdapterMixin定义于 src/diffusers/loaders/peft.py,其文档字符串明确声明了它提供的四类能力:
- 附加新适配器到模型中;
- 附加多个适配器,并可迭代式地激活/停用它们;
- 激活/停用模型中的所有适配器;
- 获取当前激活的适配器列表。
从源码结构看,该类实现的方法可归为以下几组:
| 能力分组 | 方法 | 核心作用 |
|---|---|---|
| 加载 | load_lora_adapter | 从 Hub / 本地目录 / state_dict 加载 LoRA 适配器 |
| 训练 | add_adapter | 注入一个空适配器(配合 PEFT 配置)用于训练 |
| 激活切换 | set_adapter/set_adapters/active_adapters | 设置单个或多个激活适配器及权重 |
| 启停 | enable_adapters/disable_adapters/enable_lora/disable_lora | 临时启用或停用适配器 |
| 融合 | fuse_lora/unfuse_lora | 将 LoRA 权重合并进基础权重,或撤销融合 |
| 卸载删除 | unload_lora/delete_adapters | 完全移除适配器 |
| 保存 | save_lora_adapter | 序列化指定适配器权重 |
| 热替换 | enable_lora_hotswap | 为 torch.compile 模型准备 LoRA 热替换 |
三、load_lora_adapter:模型级 LoRA 加载的完整流程
load_lora_adapter是PeftAdapterMixin的核心方法(peft.py#L79-L367),它把「加载适配器」这个高层语义拆解为一条清晰的流水线。
3.1 参数说明
| 参数 | 类型/默认值 | 说明 |
|---|---|---|
pretrained_model_name_or_path_or_dict | str/os.PathLike/dict | 可以是 Hub 上的模型 id(如google/ddpm-celebahq-256)、本地目录路径(配合ModelMixin.save_pretrained保存的权重),或一个 PyTorch state dict |
prefix | str,可选 | 用于过滤 state dict 的前缀。例如只加载 UNet 相关键时传"unet" |
cache_dir | str,可选 | 自定义模型缓存目录 |
force_download | bool,默认False | 强制重新下载权重,忽略缓存 |
proxies | dict,可选 | 按协议/端点配置代理服务器 |
local_files_only | bool,默认False | 仅加载本地文件,不从 Hub 下载 |
token | str/bool,可选 | Hub 鉴权 token,True时使用diffusers-cli login生成的 token |
revision | str,默认"main" | 指定模型版本(分支名、tag 或 commit id) |
subfolder | str,默认"" | 大模型仓库中模型文件的子目录 |
network_alphas | dict[str, float] | 网络 alpha 值,语义等同于 kohya-ss 训练脚本中的--network_alpha,用于稳定训练、防止下溢 |
low_cpu_mem_usage | bool,默认False | 只加载 LoRA 权重而不初始化随机权重,加快加载速度 |
hotswap | bool,默认False | 是否用新适配器就地替换已有适配器,避免 torch.compile 模型重编译 |
metadata | dict,可选 | LoRA 适配器元数据;提供时优先用它初始化LoraConfig,不再从 state dict 推断 |
3.2 内部流水线(源码级拆解)
- 获取 state dict:通过
_fetch_state_dict(定义于 src/diffusers/loaders/lora_base.py)下载或读取权重,user_agent标记为{"file_type": "attn_procs_weights", "framework": "pytorch"},并默认禁止 pickle(allow_pickle = False,即优先使用 safetensors)。 - 前缀过滤:若指定了
prefix,则用k.removeprefix(f"{prefix}.")过滤出对应模块的键(peft.py#L194-L197)。 - 格式归一化:检查第一个键,若不含
"lora_A",则调用convert_unet_state_dict_to_peft把 diffusers 格式转换为 PEFT 格式;若检测到"lora_controlnet"键(即 Stability AI 的 Control LoRA),则调用convert_sai_sd_control_lora_state_dict_to_peft专门处理(peft.py#L210-L220)。 - 推断 rank:遍历 state dict 中所有
"lora_B"且维度大于 1 的键,用val.shape[1]收集每个模块的 rank(peft.py#L222-L231)。注意注释中提到 PEFT 的^符号约定,用于区分具有公共前缀的模块名(例如proj_out.weight与blocks.transformer.proj_out.weight可能 rank 不同)。 - 自动命名:未传
adapter_name时,通过get_adapter_name生成default_0、default_1之类的默认名(依据已加载适配器数量计数)。 - 构建 LoraConfig:调用
_create_lora_config(见 src/diffusers/utils/peft_utils.py#L371-L391)生成 PEFT 的LoraConfig。 - 注入与赋值:调用 PEFT 的
inject_adapter_in_model注入 LoRA 层,再用set_peft_model_state_dict写入权重;失败时回滚peft_config并清理已注入的BaseTunerLayer(peft.py#L294-L344)。 - 兼容 offload:通过
_optionally_disable_offloading在加载前临时移除模型的 CPU offload hook(模型 offload、顺序 offload 或 group offload),加载完成后再恢复(peft.py#L348-L357)。 - 未匹配键告警:
_maybe_warn_for_unhandled_keys会对.lora_相关的 unexpected/missing keys 给出警告,帮助排查适配器与模型不匹配的问题(peft_utils.py#L394-L418)。
3.3 实际使用示例
load_lora_adapter是模型级方法,直接从 UNet 上调用。若只加载到 UNet,text encoder 的键会被忽略;此时用prefix="unet"过滤对应的 state dict:
import torch from diffusers import AutoPipelineForText2Image pipeline = AutoPipelineForText2Image.from_pretrained( "stabilityai/stable-diffusion-xl-base-1.0", dtype=torch.float16, ).to("cuda") # 也支持 "mps"、"xpu"、"cpu" pipeline.unet.load_lora_adapter( "jbilcke-hf/sdxl-cinematic-1", weight_name="pytorch_lora_weights.safetensors", adapter_name="cinematic", prefix="unet", ) # 在提示词中使用 "cnmt" 触发该 LoRA pipeline("A cute cnmt eating a slice of pizza, stunning color scheme, masterpiece, illustration").images[0]要点:load_lora_adapter构建并准备了适配器所需的完整模型配置,这也是它与 Pipeline 层load_lora_weights(需要处理 UNet 与 text encoder 双标识符)的主要区别——前者更聚焦、更底层。
四、LoraConfig 自动推导:rank 与 alpha 的推断逻辑
从源码看,PeftAdapterMixin的一个关键设计是从 state dict 自动推导 LoraConfig,而不是要求用户手动指定r、lora_alpha等超参数。核心实现在get_peft_kwargs(src/diffusers/utils/peft_utils.py#L153-L208),其逻辑为:
- 若所有模块 rank 一致,直接令
r = lora_alpha = 该 rank; - 若 rank 不一致,取出现次数最多的 rank 作为全局
r,其余模块写入rank_pattern;当 checkpoint 中没有 alpha 数据时,按 diffusers/PEFT 约定W_eff = W + lora_B @ lora_A(即每个模块alpha == rank,缩放为 1.0),把rank_pattern镜像到alpha_pattern以保持每模块 scale 为 1.0; - 若提供了
network_alphas,则按同样「取众数、其余进alpha_pattern」的方式处理; - 通过
"lora_magnitude_vector"键检测 DoRA(use_dora),通过"lora_B"后缀.bias检测lora_bias; target_modules由 state dict 中所有.lora前缀的模块名去重得到。
这些推断规则在仓库测试 tests/lora/test_peft_utils.py 中有明确的验证用例,例如:
test_mixed_ranks_without_alphas_apply_at_scale_one:不同模块 rank 各异且无 alpha 键时,所有模块有效缩放alpha / rank必须等于 1.0;test_uniform_rank_without_alphas_unchanged:rank 一致时r == lora_alpha == rank,且rank_pattern与alpha_pattern为空;test_mixed_ranks_with_uniform_alpha_keep_declared_alpha:显式声明的 alpha 优先于alpha == rank约定(如 alpha=32、rank=64 的模块缩放为 0.5)。
这也解释了为什么load_lora_adapter的metadata参数有意义:当 checkpoint 自带lora_adapter_metadata元数据时,_create_lora_config直接采用元数据,跳过上述推断,避免歧义。
五、多适配器的激活、切换与权重控制
PeftAdapterMixin提供了完整的适配器激活管理 API,这在同时使用多个 LoRA 时尤其重要。
5.1 set_adapters:同时激活多个适配器并设置权重
set_adapters(peft.py#L437-L491)接收适配器名列表与权重列表,权重为None时默认1.0。实现上通过set_weights_and_activate_adapters(peft_utils.py#L254-L283)逐模块调用 PEFT 的set_adapter与set_scale。
值得注意的是源码中的_SET_ADAPTER_SCALE_FN_MAPPING(peft.py#L47-L53):对UNet2DConditionModel和UNetMotionModel,权重会先经过_maybe_expand_lora_scales展开(该函数定义于 src/diffusers/loaders/unet_loader_utils.py),以支持对 UNet 各 down/mid/up 块做细粒度缩放;其他模型类则直接使用原权重。
pipeline.load_lora_weights( "jbilcke-hf/sdxl-cinematic-1", weight_name="pytorch_lora_weights.safetensors", adapter_name="cinematic", ) pipeline.load_lora_weights( "nerijs/pixel-art-xl", weight_name="pixel-art-xl.safetensors", adapter_name="pixel", ) pipeline.unet.set_adapters(["cinematic", "pixel"], weights=[0.5, 0.5])5.2 LoRA 缩放(Weight scale)
scale参数控制 LoRA 的应用强度:0等价于只用基础模型,1等价于完全使用 LoRA。
- 简单场景:通过
cross_attention_kwargs={"scale": 1.0}传入 Pipeline。其底层由apply_lora_scale装饰器(peft_utils.py#L286-L332)实现——它从attention_kwargs中弹出scale,在 forward 前调用scale_lora_layers缩放、结束后在finally中调用unscale_lora_layers恢复,保证异常时也能正确还原。 - 细粒度控制:向
set_adapters传字典,可分别指定 text encoder 与 UNet 各块的缩放:
scales = { "text_encoder": 0.5, "text_encoder_2": 0.5, "unet": { "down": 0.9, "up": { "block_0": 0.6, "block_1": [0.4, 0.8, 1.0], }, }, } pipeline.set_adapters("cereal", scales)[!WARNING]
set_adapters只缩放注意力权重。若 LoRA 还包含 ResNet、下采样/上采样等组件,这些组件会保持1.0的缩放;未显式指定的块(如"mid")默认使用1.0。
5.3 单一适配器的设置与查询
set_adapter(adapter_name):强制模型只使用指定适配器、停用其他适配器。传入不存在的名字会抛出ValueError并列出当前已加载的适配器;旧版 PEFT 不支持多适配器推理时,设置多个适配器会提示升级(peft.py#L531-L578)。active_adapters():返回当前激活的适配器列表,实现是遍历模块找到第一个BaseTunerLayer并读取其active_adapter(peft.py#L625-L644)。enable_adapters()/disable_adapters():分别启用/停用模型上所有适配器。停用后模型回退到仅使用基础权重,但适配器仍然保留在模型上,可随时重新启用(peft.py#L580-L623)。
六、LoRA 融合:fuse_lora / unfuse_lora
fuse_lora(peft.py#L646-L674)将 LoRA 权重直接合并进基础模型权重(UNet 与 text encoder),从而:
- 只需加载一次基础模型即可服务多个 LoRA,降低内存占用;
- 合并后推理速度更快(省去 LoRA 分支计算)。
关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
lora_scale | 1.0 | 融合时的输出缩放比例,需要在此刻确定——融合后cross_attention_kwargs={"scale": ...}不再生效 |
safe_fusing | False | 融合前检查权重中是否存在 NaN,存在则跳过该层(safe merge) |
adapter_names | None | 指定要融合的适配器名;旧版 PEFT 不支持该参数时会要求升级 |
实现细节:fuse_lora遍历所有BaseTunerLayer,先按lora_scale调用scale_layer,再调用 PEFT 的merge完成权重合并(若为safe_fusing则传safe_merge=True)。
pipeline.set_adapters(["ikea", "feng"], adapter_weights=[0.7, 0.8]) pipeline.fuse_lora(adapter_names=["ikea", "feng"], lora_scale=1.0) # 融合后即可卸载 LoRA 权重 pipeline.unload_lora() pipeline.save_pretrained("path/to/fused-pipeline")unfuse_lora执行逆向操作unmerge,把基础权重恢复原状。需要特别注意的是:只有在只融合了一个 LoRA 时才能 unfuse;多个 LoRA 融合后无法逐一撤销,只能重新加载整个模型。
七、torch.compile 与 LoRA 热替换(Hotswapping)
7.1 编译前先融合
torch.compile通过生成优化内核加速推理。由于编译后的计算图对结构敏感,使用 PEFT 时必须先把 LoRA 融合进基础模型并卸载权重,再执行编译:
pipeline.load_lora_weights( "ostris/ikea-instructions-lora-sdxl", weight_name="ikea_instructions_xl_v1_5.safetensors", adapter_name="ikea", ) pipeline.set_adapters("ikea", adapter_weights=0.7) pipeline.fuse_lora(adapter_names=["ikea"], lora_scale=1.0) pipeline.unload_lora() pipeline.unet.to(memory_format=torch.channels_last) pipeline.unet = torch.compile(pipeline.unet, mode="reduce-overhead", fullgraph=True)通常只编译计算量最大的 UNet 即可获得大部分收益。
7.2 Hotswapping:免重编译切换 LoRA
Hotswapping 是「就地替换」已加载适配器权重的机制:新 LoRA 的权重直接写入已有适配器的参数槽位,而不是追加新参数,因此不会触发 torch.compile 重编译,也不会随多次load_lora_weights调用累积内存。这是多 LoRA 场景下的高效工作流,前提是先加载过一个 LoRA。
load_lora_adapter的hotswap参数(peft.py#L279-L310)实现细节:
- 校验
adapter_name必须是已存在的适配器名(否则报错提示设置hotswap=False); - 通过
map_state_dict_for_hotswap在 state dict 键中补入适配器名(lora_A.weight→lora_A.<name>.weight,lora_B.bias同理); - 调用
check_hotswap_configs_compatible校验新旧适配器配置兼容性; - 调用 PEFT 的
hotswap_adapter_from_state_dict完成就地替换。
[!WARNING] Hotswapping 目前不支持作用于 text encoder 的 LoRA,且新 LoRA 只能作用于第一个 LoRA 覆盖的层或其子集。因此加载顺序很重要——建议把覆盖层数最多的 LoRA 放在第一个加载。
7.3 enable_lora_hotswap:不同 rank / 编译模型的准备
当模型已编译,或后续要热替换的 LoRA 与首个 LoRA 的 rank、alpha 不同时,需要调用enable_lora_hotswap(peft.py#L786-L817):
| 参数 | 默认值 | 说明 |
|---|---|---|
target_rank | 128 | 所有将被加载适配器的最高 rank;不同 rank 的 LoRA 应设为较大值 |
check_compiled | "error" | 模型已编译时的处理方式:"error"抛错、"warn"告警、"ignore"忽略 |
调用顺序非常关键:必须在加载第一个 LoRA之前调用enable_lora_hotswap,并在加载第一个 LoRA之后再执行torch.compile,随后方可对后续 LoRA 使用hotswap=True:
# 1. 先启用 hotswap(必须在加载首个适配器之前) pipeline.enable_lora_hotswap(target_rank=max_rank) pipeline.load_lora_weights( "ostris/ikea-instructions-lora-sdxl", weight_name="ikea_instructions_xl_v1_5.safetensors", adapter_name="ikea", ) # 2. 再编译 pipeline.unet = torch.compile(pipeline.unet, mode="reduce-overhead", fullgraph=True) # 3. 热替换 pipeline.load_lora_weights( "lordjia/by-feng-zikai", hotswap=True, adapter_name="ikea", )其底层原理(由 src/diffusers/loaders/peft.py 中_prepare_lora_hotswap_kwargs与 PEFT 的prepare_model_for_compiled_hotswap配合实现):把 LoRA 缩放因子从 float 转成 torch tensor,并将权重张量 padding 到最大所需形状,使权重替换时无需重新分配整个属性、不改变计算图结构。虽然零填充不改变计算结果,但 padding 越大计算越慢。
调试提示:将代码放入with torch._dynamo.config.patch(error_on_recompile=True):上下文,可检测模型是否发生了重编译;若在推理中预期出现多种分辨率,编译时应设置dynamic=True。
八、适配器的保存、卸载与删除
8.1 save_lora_adapter:序列化指定适配器
save_lora_adapter(peft.py#L369-L435)将指定适配器权重保存为文件:
| 参数 | 默认值 | 说明 |
|---|---|---|
save_directory | 必填 | 保存目录,不存在则自动创建;若传入的是文件路径会报错 |
adapter_name | "default" | 要序列化的适配器名,多适配器模型下用于区分 |
upcast_before_saving | False | 序列化前是否将模型转为torch.float32 |
safe_serialization | True | 使用 safetensors 保存(pytorch_lora_weights.safetensors);为False时用 pickle(pytorch_lora_weights.bin) |
weight_name | None | 自定义权重文件名 |
实现上通过 PEFT 的get_peft_model_state_dict收集指定适配器的 LoRA 参数,并把peft_config序列化后写入 safetensors 元数据(键为lora_adapter_metadata),确保保存的 checkpoint 可被load_lora_adapter完整还原。
8.2 unload_lora / delete_adapters / disable_lora
三者的区别在于「移除」的程度:
disable_lora()/enable_lora():临时停用/启用 LoRA 层,权重保留在模型上,可随时恢复,用于 A/B 对比基础模型与 LoRA 效果(peft.py#L702-L746);unload_lora():通过recurse_remove_peft_layers(peft_utils.py#L35-L103)递归地把BaseTunerLayer替换回基础层,删除peft_config,并处理 group offload 的重挂载;常用于 LoRA 融合后的清理(peft.py#L687-L700);delete_adapters(names):针对性地删除指定适配器的 LoRA 层,并同步从peft_config中弹出对应条目;删除全部适配器后peft_config整体移除、_hf_peft_config_loaded复位(peft.py#L748-L784)。
九、训练侧:add_adapter
除了推理,PeftAdapterMixin也支持训练场景。add_adapter(peft.py#L493-L529)将一个空的适配器注入当前模型用于训练:
- 接收一个 PEFT 的
PeftConfig实例(如LoraConfig)和适配器名(默认"default"); - 注入成功后立即调用
set_adapter激活它; - 要求 PEFT 版本不低于
MIN_PEFT_VERSION(当前仓库中该常量为"0.17.0",见 src/diffusers/utils/constants.py),否则抛出版本不兼容错误。
十、哪些模型支持 PeftAdapterMixin
PeftAdapterMixin被大量 Diffusers 模型类继承,覆盖 UNet、Transformer、ControlNet 等主流架构。从源码检索可见,其使用者包括但不限于:
UNet2DConditionModel(src/diffusers/models/unets/unet_2d_condition.py#L76-L78,定义处同时继承了UNet2DConditionLoadersMixin,因此 Pipeline 层的load_lora_weights最终也落到 PEFT 后端);SD3Transformer2DModel(src/diffusers/models/transformers/transformer_sd3.py)等各代 Transformer(FLUX、Hunyuan Video、CogVideoX、Sana、Mochi、Wan、QwenImage、Lumina2 等);- ControlNet 系列(src/diffusers/models/controlnets/controlnet.py、controlnet_flux、controlnet_sd3 等);
- 自编码器(AutoencoderKL 等)与条件编码器。
从源码结构看,这套 mixin 的设计使其可以「即插即用」地挂载到任何继承它的 Diffusers 模型上,这也是它比 Pipeline 层 API 更具通用性的原因。
十一、安装与环境要求
使用PeftAdapterMixin前需安装 PEFT 库:
pip install -U diffusers peft- PEFT 后端相关功能(
set_adapters、fuse_lora、unload_lora等)在未启用 PEFT 后端时会抛出ValueError: PEFT backend is required ...; - 仓库要求的 PEFT 最低版本为
0.17.0(src/diffusers/utils/constants.py),add_adapter、set_adapter等方法会通过check_peft_version做版本校验; - 若 PEFT 未安装,
add_adapter/active_adapters会提示pip install peft。
十二、总结与进阶指引
PeftAdapterMixin是 Diffusers 模型层适配器能力的统一出口,围绕它形成了完整的 LoRA 生命周期:加载(load_lora_adapter)→ 配置自动推导(rank/alpha)→ 多适配器激活与缩放(set_adapters)→ 融合加速(fuse_lora)→ 编译与热替换(enable_lora_hotswap)→ 保存/卸载/删除。
继续深入可参考:
- 推理实战完整教程:Inference with PEFT,涵盖文生图、文生视频、LoRA 合并(
add_weighted_adapter、TIES/DARE 方法)等场景; - 核心实现:src/diffusers/loaders/peft.py 与 src/diffusers/utils/peft_utils.py;
- 单元测试:tests/lora/test_peft_utils.py,可对照验证 rank/alpha 推断规则。
【免费下载链接】diffusers🤗 Diffusers: State-of-the-art diffusion models for image, video, and audio generation in PyTorch.项目地址: https://gitcode.com/GitHub_Trending/di/diffusers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考