LoRA
LoRA (Low-Rank Adaptation) 是一种让模型快速适配新任务的方法。它会冻结原始模型权重,并额外添加一小部分新的可训练参数。这样一来,在现有模型上适配新任务的速度会更快、成本也更低,比如生成某种新的图像风格。
LoRA的checkpoint通常只有几百 MB,因此非常轻量,也很容易存储。你可以使用 load_lora_weights() 将这组较小的权重加载到现有基础模型中,并通过 weight_name 指定文件名。
import torch
from diffusers import AutoPipelineForText2Image
pipeline = AutoPipelineForText2Image.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
"ostris/super-cereal-sdxl-lora",
weight_name="cereal_box_sdxl_v1.safetensors",
adapter_name="cereal"
)
pipeline("bears, pizza bites").images[0]load_lora_weights() 是把 LoRA 权重加载到 UNet 和 text encoder 中的首选方式,因为它能处理以下情况:
- LoRA 权重没有分别标注 UNet 和text encoder标识符
- LoRA 权重分别带有 UNet 和text encoder标识符
load_lora_adapter() 则用于在模型级别直接加载 LoRA adapter,只要该模型是 Diffusers 模型并且继承自 PeftAdapterMixin 即可。它会为 adapter 构建并准备所需的模型配置。这个方法同样会把 LoRA adapter 加载到 UNet 中。
例如,如果你只想把 LoRA 加载到 UNet,load_lora_adapter() 会忽略文本编码器对应的 key。使用 prefix 参数筛选并加载合适的 state dict,这里传入 "unet" 即可。
import torch
from diffusers import AutoPipelineForText2Image
pipeline = AutoPipelineForText2Image.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
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]torch.compile
torch.compile 会通过编译 PyTorch 模型来使用优化内核,从而加速推理。在编译之前,需要先把 LoRA 权重融合进基础模型,并卸载原始 LoRA 权重。
import torch
from diffusers import DiffusionPipeline
# 加载基础模型和 LoRA
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
"ostris/ikea-instructions-lora-sdxl",
weight_name="ikea_instructions_xl_v1_5.safetensors",
adapter_name="ikea"
)
# 激活 LoRA 并设置 adapter 权重
pipeline.set_adapters("ikea", adapter_weights=0.7)
# 融合 LoRA 并卸载权重
pipeline.fuse_lora(adapter_names=["ikea"], lora_scale=1.0)
pipeline.unload_lora_weights()通常会编译 UNet,因为它是整个管道里计算最密集的部分。
pipeline.unet.to(memory_format=torch.channels_last)
pipeline.unet = torch.compile(pipeline.unet, mode="reduce-overhead", fullgraph=True)
pipeline("A bowl of ramen shaped like a cute kawaii bear").images[0]如果你想在编译模型后配合多个 LoRA 一起使用,又不想每次都重新编译,可以查看下文的 hotswapping 部分。
权重缩放
scale 参数用于控制 LoRA 的应用强度。值为 0 时等价于只使用基础模型权重;值为 1 时等价于完全使用 LoRA。
对于简单场景,可以直接把 cross_attention_kwargs={"scale": 1.0} 传给管道。
import torch
from diffusers import AutoPipelineForText2Image
pipeline = AutoPipelineForText2Image.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
"ostris/super-cereal-sdxl-lora",
weight_name="cereal_box_sdxl_v1.safetensors",
adapter_name="cereal"
)
pipeline("bears, pizza bites", cross_attention_kwargs={"scale": 1.0}).images[0]缩放调度
在采样过程中动态调整 LoRA scale,通常可以让你更好地控制整体构图和布局,因为某些采样步骤可能更适合使用更高或更低的 scale。
下面的例子使用了一个 character LoRA。它在前 20 步使用较高的 scale,并逐步衰减,以便先把角色生成出来;在后续步骤中,只保留 0.2 的 scale,避免把 LoRA 学到的特征过多地施加到图像中其他并非训练目标的区域。
import torch
from diffusers import FluxPipeline
pipeline = FluxPipeline.from_pretrained(
"black-forest-labs/FLUX.1-dev", torch_dtype=torch.bfloat16
).to("cuda")
pipelne.load_lora_weights("alvarobartt/ghibli-characters-flux-lora", "lora")
num_inference_steps = 30
lora_steps = 20
lora_scales = torch.linspace(1.5, 0.7, lora_steps).tolist()
lora_scales += [0.2] * (num_inference_steps - lora_steps + 1)
pipeline.set_adapters("lora", lora_scales[0])
def callback(pipeline: FluxPipeline, step: int, timestep: torch.LongTensor, callback_kwargs: dict):
pipeline.set_adapters("lora", lora_scales[step + 1])
return callback_kwargs
prompt = """
Ghibli style The Grinch, a mischievous green creature with a sly grin, peeking out from behind a snow-covered tree while plotting his antics,
in a quaint snowy village decorated for the holidays, warm light glowing from cozy homes, with playful snowflakes dancing in the air
"""
pipeline(
prompt=prompt,
guidance_scale=3.0,
num_inference_steps=num_inference_steps,
generator=torch.Generator().manual_seed(42),
callback_on_step_end=callback,
).images[0]热切换
LoRA 热切换(hotswapping)是一种高效的多 LoRA 工作方式。它可以避免多次调用 load_lora_weights() 带来的额外内存累积;在某些情况下,如果模型已经编译,还可以避免重新编译。这个工作流要求你先加载一个 LoRA,因为新的 LoRA 权重会原地替换当前已加载的 LoRA。
import torch
from diffusers import DiffusionPipeline
# 加载基础模型和 LoRA
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
"ostris/ikea-instructions-lora-sdxl",
weight_name="ikea_instructions_xl_v1_5.safetensors",
adapter_name="ikea"
)目标是文本编码器的 LoRA 目前不支持热切换。
在 load_lora_weights() 中设置 hotswap=True,即可替换第二个 LoRA。使用 adapter_name 参数指定要替换的是哪个 LoRA(默认名字是 default_0)。
pipeline.load_lora_weights(
"lordjia/by-feng-zikai",
hotswap=True,
adapter_name="ikea"
)编译模型
对于已经编译的模型,可以使用 enable_lora_hotswap() 来避免热切换时重新编译。这个方法应该在加载第一个 LoRA 之前调用,而 torch.compile 则应该在加载第一个 LoRA 之后调用。
如果第二个 LoRA 与第一个 LoRA 的 rank 和 scale 完全一致,那么
enable_lora_hotswap()不一定是必需的。
在 enable_lora_hotswap() 中,target_rank 参数很重要,它决定了所有 LoRA adapter 的 rank。设为 max_rank 时,会自动取最大的 rank;如果 LoRA 的 rank 不同,你也可以手动设为更高的值。默认 rank 是 128。
import torch
from diffusers import DiffusionPipeline
# 加载基础模型和 LoRA
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
# 1. 启用 enable_lora_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. torch.compile
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"
)你可以把代码放进
with torch._dynamo.config.patch(error_on_recompile=True)上下文中,用来检测模型是否发生了重新编译。如果你严格按照上面的步骤做了,模型依然重新编译,请带着可复现示例提交一个 issue。
如果你预计在推理时会使用不同分辨率,请在编译时设置 dynamic=True。更多细节可以参考这篇文档。
有些情况下,重新编译依然无法避免,例如热切换进来的 LoRA 比初始 adapter 覆盖了更多层。这时,尽量先加载那个覆盖层数最多的 LoRA。关于这个限制的更多说明,可以参考 PEFT 的 hotswapping 文档。
热切换的技术细节
enable_lora_hotswap() 会把 LoRA 的缩放因子从 float 转成 torch.tensor,并把权重形状补齐到所需的最大形状,这样在替换权重数据时,就不用重新分配整个属性。
这也是为什么 max_rank 参数很重要。即使补出来的部分是零,也不会改变最终结果,只是补齐量越大,计算速度可能会更慢一些。
由于不会新增新的 LoRA 属性,因此后续热切换进来的 LoRA 只能作用于与第一个 LoRA 相同的层,或者其子集。LoRA 的加载顺序因此会很关键。如果多个 LoRA 的目标层彼此不相交,你最终可能需要先构造一个覆盖所有目标层并集的 dummy LoRA。
如果想了解更多实现细节,可以直接查看 hotswap.py 文件。
合并
你可以把多个 LoRA 的权重合并在一起,得到多种现有风格的混合效果。LoRA 合并有多种方法,不同方法主要区别在于如何合并权重,这也可能影响生成质量。
set_adapters
set_adapters() 会通过拼接多个 LoRA 的加权矩阵来完成合并。把 LoRA 名称传给 set_adapters(),再通过 adapter_weights 参数控制每个 LoRA 的缩放权重。例如,当 adapter_weights=[0.5, 0.5] 时,输出就是两个 LoRA 的平均效果。
"scale"参数决定了应用合并后 LoRA 的强度。详情可参考前面的 权重缩放 部分。
import torch
from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
"ostris/ikea-instructions-lora-sdxl",
weight_name="ikea_instructions_xl_v1_5.safetensors",
adapter_name="ikea"
)
pipeline.load_lora_weights(
"lordjia/by-feng-zikai",
weight_name="fengzikai_v1.0_XL.safetensors",
adapter_name="feng"
)
pipeline.set_adapters(["ikea", "feng"], adapter_weights=[0.7, 0.8])
# 在提示词中使用 by Feng Zikai 来激活 lordjia/by-feng-zikai 这个 LoRA
pipeline("A bowl of ramen shaped like a cute kawaii bear, by Feng Zikai", cross_attention_kwargs={"scale": 1.0}).images[0]
add_weighted_adapter
这是一个实验性方法。更多背景可以参考 PEFT 的 Model merging 文档。如果你想了解这项集成背后的动机和设计,也可以看看这个 issue。
add_weighted_adapter 支持使用更高效的合并方法,比如 TIES 或 DARE。这些方法会从合并后的模型中移除冗余或可能互相干扰的参数。需要注意的是,要进行合并,各个 LoRA 的 rank 必须一致。
请先确保安装的是最新版稳定版 Diffusers 和 PEFT。
pip install -U -q diffusers peft
先加载一个与 LoRA UNet 对应的 UNet。
import copy
import torch
from diffusers import AutoModel, DiffusionPipeline
from peft import get_peft_model, LoraConfig, PeftModel
unet = AutoModel.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16,
use_safetensors=True,
variant="fp16",
subfolder="unet",
).to("cuda")加载一个管道,把这个 UNet 传进去,然后再加载 LoRA。
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
variant="fp16",
torch_dtype=torch.float16,
unet=unet
).to("cuda")
pipeline.load_lora_weights(
"ostris/ikea-instructions-lora-sdxl",
weight_name="ikea_instructions_xl_v1_5.safetensors",
adapter_name="ikea"
)通过前面加载的第一个 UNet 和管道中的 LoRA UNet,创建一个来自该 LoRA 检查点的 PeftModel。
sdxl_unet = copy.deepcopy(unet)
ikea_peft_model = get_peft_model(
sdxl_unet,
pipeline.unet.peft_config["ikea"],
adapter_name="ikea"
)
original_state_dict = {f"base_model.model.{k}": v for k, v in pipeline.unet.state_dict().items()}
ikea_peft_model.load_state_dict(original_state_dict, strict=True)你也可以像下面这样把
ikea_peft_model推送到 Hub,之后保存并复用。ikea_peft_model.push_to_hub("ikea_peft_model", token=TOKEN)
重复这一步,为第二个 LoRA 再创建一个 PeftModel。
pipeline.delete_adapters("ikea")
sdxl_unet.delete_adapters("ikea")
pipeline.load_lora_weights(
"lordjia/by-feng-zikai",
weight_name="fengzikai_v1.0_XL.safetensors",
adapter_name="feng"
)
pipeline.set_adapters(adapter_names="feng")
feng_peft_model = get_peft_model(
sdxl_unet,
pipeline.unet.peft_config["feng"],
adapter_name="feng"
)
original_state_dict = {f"base_model.model.{k}": v for k, v in pipe.unet.state_dict().items()}
feng_peft_model.load_state_dict(original_state_dict, strict=True)加载一个基础 UNet,并加载 adapters。
base_unet = AutoModel.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16,
use_safetensors=True,
variant="fp16",
subfolder="unet",
).to("cuda")
model = PeftModel.from_pretrained(
base_unet,
"stevhliu/ikea_peft_model",
use_safetensors=True,
subfolder="ikea",
adapter_name="ikea"
)
model.load_adapter(
"stevhliu/feng_peft_model",
use_safetensors=True,
subfolder="feng",
adapter_name="feng"
)使用 add_weighted_adapter 合并 LoRA,并通过 combination_type 指定合并方式。下面的例子使用 "dare_linear" 方法(想了解这些合并方法,可以参考这篇博客),它会先随机裁剪一部分权重,再根据 weights 中给定的权重,对各个 LoRA 的张量做加权求和。
再使用 set_adapters() 激活合并后的 LoRA。
model.add_weighted_adapter(
adapters=["ikea", "feng"],
combination_type="dare_linear",
weights=[1.0, 1.0],
adapter_name="ikea-feng"
)
model.set_adapters("ikea-feng")
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
unet=model,
variant="fp16",
torch_dtype=torch.float16,
).to("cuda")
pipeline("A bowl of ramen shaped like a cute kawaii bear, by Feng Zikai").images[0]
fuse_lora
fuse_lora() 会把 LoRA 权重直接融合到基础模型底层的 UNet 和文本编码器权重中。这样做可以减少每个 LoRA 都重新加载底层模型的开销,因为基础模型只需加载一次,从而降低内存占用并提升推理速度。
import torch
from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
"ostris/ikea-instructions-lora-sdxl",
weight_name="ikea_instructions_xl_v1_5.safetensors",
adapter_name="ikea"
)
pipeline.load_lora_weights(
"lordjia/by-feng-zikai",
weight_name="fengzikai_v1.0_XL.safetensors",
adapter_name="feng"
)
pipeline.set_adapters(["ikea", "feng"], adapter_weights=[0.7, 0.8])调用 fuse_lora() 进行融合。lora_scale 参数控制 LoRA 权重对输出的缩放强度。这里必须现在就设置好,因为在这个场景下,向 cross_attention_kwargs 传 scale 不会生效。
pipeline.fuse_lora(adapter_names=["ikea", "feng"], lora_scale=1.0)由于 LoRA 权重已经融合到底层模型中,可以把它们卸载掉。然后通过 save_pretrained() 保存到本地,或者通过 ~PushToHubMixin.push_to_hub 保存到 Hub。
pipeline.unload_lora_weights()
pipeline.save_pretrained("path/to/fused-pipeline")之后,你就可以快速加载这个融合后的管道进行推理,而不需要分别加载每个 LoRA。
pipeline = DiffusionPipeline.from_pretrained(
"username/fused-ikea-feng", torch_dtype=torch.float16,
).to("cuda")
pipeline("A bowl of ramen shaped like a cute kawaii bear, by Feng Zikai").images[0]如果你想恢复底层模型原始权重,例如想改用不同的 lora_scale,可以使用 unfuse_lora()。不过只有融合了单个 LoRA 时才能反融合。比如上面那个含多个融合 LoRA 的管道就无法这样做,这种情况下你需要重新加载整个模型。
pipeline.unfuse_lora()

管理
Diffusers 提供了多种方法来帮助你管理 LoRA,尤其是在同时使用多个 LoRA 时会很有帮助。
set_adapters
set_adapters() 也会在多个活跃 LoRA 中激活当前要使用的那个 LoRA。你可以通过指定名字,在不同 LoRA 之间切换。
import torch
from diffusers import DiffusionPipeline
pipeline = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
pipeline.load_lora_weights(
"ostris/ikea-instructions-lora-sdxl",
weight_name="ikea_instructions_xl_v1_5.safetensors",
adapter_name="ikea"
)
pipeline.load_lora_weights(
"lordjia/by-feng-zikai",
weight_name="fengzikai_v1.0_XL.safetensors",
adapter_name="feng"
)
# 激活 feng LoRA,而不是 ikea LoRA
pipeline.set_adapters("feng")save_lora_adapter
使用 save_lora_adapter() 保存 adapter。
import torch
from diffusers import AutoPipelineForText2Image
pipeline = AutoPipelineForText2Image.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
).to("cuda")
pipeline.unet.load_lora_adapter(
"jbilcke-hf/sdxl-cinematic-1",
weight_name="pytorch_lora_weights.safetensors",
adapter_name="cinematic"
prefix="unet"
)
pipeline.save_lora_adapter("path/to/save", adapter_name="cinematic")unload_lora_weights
unload_lora_weights() 会卸载管道中的所有 LoRA 权重,并恢复到底层模型原始权重。
pipeline.unload_lora_weights()
disable_lora
disable_lora() 会禁用所有 LoRA(但仍保留在管道中),并让管道恢复到底层模型权重。
pipeline.disable_lora()
get_active_adapters
get_active_adapters() 会返回挂载在管道上的活跃 LoRA 列表。
pipeline.get_active_adapters()
["cereal", "ikea"]get_list_adapters
get_list_adapters() 会返回管道中每个组件当前有哪些活跃 LoRA。
pipeline.get_list_adapters()
{"unet": ["cereal", "ikea"], "text_encoder_2": ["cereal"]}delete_adapters
delete_adapters() 会把某个 LoRA 及其对应层从模型中彻底移除。
pipeline.delete_adapters("ikea")资源
你可以在 LoRA Studio 浏览可用的 LoRA,也可以使用下面这个 Civitai Space,把自己喜欢的 LoRA 上传到 Hub。
你还可以在 FLUX LoRA the Explorer 和 LoRA the Explorer 这两个仓库中找到更多 LoRA。
如果你想了解如何结合 FlashAttention-3 和 fp8 量化等方法优化 LoRA 推理,也可以看看这篇博客:Fast LoRA inference for Flux with Diffusers and PEFT。
Update on GitHub