2025-10-20 · 3 min read
Loading LoRA Explicitly in the Diffusers Pipeline
The Diffusers library provides fine-grained control over LoRA loading. You can load, combine, fuse, and unload adapters programmatically for inference and experimentation.
Basic loading
from diffusers import StableDiffusionXLPipeline
import torch
# Load base model
pipe = StableDiffusionXLPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16,
use_safetensors=True,
variant="fp16"
)
pipe.to("cuda")
# Load a single LoRA
pipe.load_lora_weights("path/to/sdxl-watercolor-lora")
# Generate
prompt = "a mountain landscape in watercolor style"
image = pipe(prompt).images[0]
Simple and clean. The adapter is applied automatically.
Named adapters and hot-swap
Name each adapter for easy switching:
# Load first adapter as "watercolor"
pipe.load_lora_weights("path/to/sdxl-watercolor-lora", weight_name="watercolor")
# Load second adapter as "product"
pipe.load_lora_weights("path/to/sdxl-product-photo-lora", weight_name="product")
# Switch to first
pipe.set_adapter("watercolor")
image1 = pipe("a landscape").images[0]
# Switch to second
pipe.set_adapter("product")
image2 = pipe("a coffee mug").images[0]
# Unload
pipe.unload_lora_weights()
No restart, no reloading the base model. Adapters swap in microseconds.
Weighted fusion
Combine multiple adapters with scalar weights:
# Load both
pipe.load_lora_weights("path/to/sdxl-watercolor-lora", weight_name="watercolor")
pipe.load_lora_weights("path/to/sdxl-product-photo-lora", weight_name="product")
# Fuse them with weights
pipe.set_adapters(
adapter_names=["watercolor", "product"],
adapter_weights=[0.6, 0.4] # 60% watercolor, 40% product
)
# Generate blended result
image = pipe("a ceramic mug on a table").images[0]
The result is a visual blend of both styles. Adjust weights to control the balance.
Full workflow: swapping and comparing
from diffusers import StableDiffusionXLPipeline
import torch
pipe = StableDiffusionXLPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
)
pipe.to("cuda")
# Load two adapters
pipe.load_lora_weights("path/to/sdxl-watercolor-lora", weight_name="watercolor")
pipe.load_lora_weights("path/to/sdxl-product-photo-lora", weight_name="product")
prompt = "a landscape with mountains and lake"
# Generate with each adapter
pipe.set_adapter("watercolor")
img_watercolor = pipe(prompt, guidance_scale=7.0).images[0]
pipe.set_adapter("product")
img_product = pipe(prompt, guidance_scale=7.0).images[0]
# Generate with blend
pipe.set_adapters(["watercolor", "product"], [0.7, 0.3])
img_blend = pipe(prompt, guidance_scale=7.0).images[0]
# Compare side-by-side
from PIL import Image
combined = Image.new("RGB", (img_watercolor.width * 3, img_watercolor.height))
combined.paste(img_watercolor, (0, 0))
combined.paste(img_product, (img_watercolor.width, 0))
combined.paste(img_blend, (img_watercolor.width * 2, 0))
combined.save("comparison.png")
Memory management
LoRA weights are loaded into VRAM alongside the base model:
# Check memory usage
import torch
print(f"VRAM allocated: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
A SDXL base (6 GB) + 3 LoRAs (150 MB each) ≈ 6.5 GB. Fits on a 12 GB card; on 8 GB, you must unload adapters:
# Unload to free VRAM
pipe.unload_lora_weights()
print(f"After unload: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
Fused adapters
Merge a LoRA into the model weights for faster inference:
# Fuse current adapter
pipe.fuse_lora(unload_weights_after_fusing=False)
# Generate (faster, no LoRA overhead)
image = pipe(prompt).images[0]
# Unfuse to restore flexibility
pipe.unfuse_lora()
Fused pipelines are ~5–10% faster. Trade-off: cannot adjust alpha at runtime.
Practical example for ModelForgeLab playground
from fastapi import FastAPI
from diffusers import StableDiffusionXLPipeline
import torch
import uuid
app = FastAPI()
# Load once on startup
pipe = StableDiffusionXLPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16
)
pipe.to("cuda")
# Preload common adapters
adapters_dir = "./data/adapters/"
for adapter_name in ["sdxl-watercolor-lora", "sdxl-product-photo-lora"]:
pipe.load_lora_weights(f"{adapters_dir}/{adapter_name}", weight_name=adapter_name)
@app.post("/v1/generate")
async def generate(request):
"""Generate image with optional LoRA."""
prompt = request["prompt"]
adapter_name = request.get("adapter", None)
if adapter_name:
pipe.set_adapter(adapter_name)
else:
pipe.unload_lora_weights()
image = pipe(prompt, guidance_scale=7.0).images[0]
# Return as base64 or save
return {"image_id": str(uuid.uuid4()), "status": "complete"}
SDXL vs FLUX
FLUX support is similar but FLUX uses dual text encoders:
# FLUX with LoRA (requires diffusers nightly)
from diffusers import FluxPipeline
pipe = FluxPipeline.from_pretrained(
"black-forest-labs/FLUX.1-dev",
torch_dtype=torch.float16
)
pipe.to("cuda")
# Load FLUX LoRA
pipe.load_lora_weights("path/to/flux-linework-lora")
image = pipe(
prompt="a clean linework sketch",
guidance_scale=3.5,
num_inference_steps=50
).images[0]
Syntax is identical; weight interpretation may differ due to FLUX's higher baseline guidance.
Performance
| Configuration | Time (SDXL) | Notes |
|---|---|---|
| Base model | 2.1s | baseline |
| Base + 1 LoRA | 2.3s | 10% slower |
| Base + fused LoRA | 2.1s | no overhead |
| Base + 2 LoRA (sequential) | 2.5s | order-dependent |
Fusing pays off for static workflows. Hot-swapping adapters is cheap; the overhead is loading from disk, not computation.
Diffusers gives you full control. Use it for experimentation, A/B testing, and production pipelines where you need fine control over adapter behavior.