A DDPM that never sees a pixel. Text in, image out, 48× smaller.
Stable Diffusion is five pieces: a frozen VAE that compresses 3×512×512 into a 4×64×64 latent, a text encoder that turns your prompt into attention keys, a U-Net that predicts noise at every step, a scheduler that decides how few steps that takes, and an optional safety checker at the gate. Learn the five job descriptions, and SDXL, SD3, FLUX and the video models become substitutions in one template.
A compressor, a translator, a sculptor, a chisel, a bouncer.
The frozen VAE compresses 3×512×512 into a 4×64×64 latent and decodes it back. The text encoder turns the prompt into 77×768 embeddings. The U-Net predicts the noise in the latent, attending to the text at every level. The scheduler decides how few steps that walk takes. The safety checker is an optional gate on the output. SDXL, SD3, FLUX and the video models are substitutions in this template.
Training drops the prompt about 10% of the time, so the same U-Net predicts noise with and without conditioning. At inference the pipeline batches both — your prompt and an empty one — and amplifies the difference: eps = eps_uncond + w·(eps_cond − eps_uncond). Low w is creative and loose; the 7–8.5 band obeys the prompt; past ~9–12 the colours burn because the sampler leaves the region the VAE can decode.
w = 0 ignore · w = 1 conditional · 7.5 classic · 9–12 burn · 2 passes per step03 / ADAPTERS, NOT RETRAINING
Freeze 860 million weights. Train a percent.
A full fine-tune updates the whole U-Net and wants 20+ GB of VRAM. LoRA freezes it and trains W + α·(A@B) inside the attention layers: rank 4–16 in practice, roughly 1–3% of the base parameters, a 10–50 MB fp16 file. The adapter loads next to any compatible checkpoint, stacks with others at independent weights, and fuses into the weights when you want speed instead of swappability.
rank 4–16 · ~1–3% of 860M · 10–50 MB · 20+ GB vs 8 GB
MENTAL MODEL IN ONE SENTENCE
Stable Diffusion is a DDPM that never sees a pixel: a frozen VAE compresses the image into 16,384 latent values, a text-conditioned U-Net denoises them for ~20–50 steps, a scheduler decides how few steps that takes, and classifier-free guidance amplifies the prompt — five independently swappable pieces you can name, tune and fine-tune one at a time.
By the end you will be able to name the five pieces and the shape that crosses each edge; do the 48× arithmetic and say what stays in pixel space; explain why the latent is a manifold rather than a small image, and use that to reason about img2img strength and inpainting masks; compute the CFG blend by hand (0.20 + 7.5 × 1.00 = 7.70) and pick a guidance scale for the prompt-versus-diversity trade; justify a sampler and a step count from the wall-clock arithmetic; run text-to-image, image-to-image, inpainting and ControlNet in diffusers; and train, load, scale and stack a LoRA adapter while keeping the 860M base frozen.
01
THE 48× TRICK
Pixel space is expensive. Diffuse somewhere 48× smaller.
Every diffusion step in Lesson 10 backpropagated through a network that saw a whole image. At 512×512 in colour that is 786,432 values per step, fifty-plus steps per sample. Latent diffusion is the same DDPM with the whole thing moved into a compressed space — and that one change is what made open-weight text-to-image practical on consumer hardware.
A DDPM learns to predict the noise that was added to a sample. In pixel space the sample is the image: 3 channels of 512×512 values, and the denoiser has to spend capacity on raw pixels — gradients, noise textures, compression artefacts — before it gets to anything that looks like content. The Rombach et al. paper asked a simple question: what if a pretrained autoencoder handled perception, and the diffusion model only handled structure?
So Stable Diffusion trains a small variational autoencoder (VAE) first, and then freezes it. The VAE maps a 3×512×512 image down to a 4×64×64 grid of continuous values — one factor of 8 per side, three channels becoming four — and back. The diffusion model is trained entirely inside that grid. At sampling time, noise in the latent space becomes an image only at the very last step, when the VAE decodes.
pixel space 3 × 512 × 512 = 786,432 values per step
latent space 4 × 64 × 64 = 16,384 values per step
786,432 ÷ 16,384 = 48 ← the source's headline number
same picture, 48× fewer numbers to model, and the U-Net never meets a pixel
The 48× compression, on a canvas
Watch a 512×512 scene become a 4×64×64 latent and come back. Scrub the round-trip, flip to the four channels to see what the U-Net actually consumes, and read the two multipliers underneath.
image 3 × 512 × 512 = 786,432
latent 4 × 64 × 64 = 16,384
ratio 48× (786,432 ÷ 16,384)
per side ÷ 8 (512 → 64)
scale factor 0.18215 (SD 1.5 · SDXL: 0.13025)
sampling budget
pixel DDIM-50 39,321,600 value-updates
latent DPM-25 409,600 value-updates
ratio 96×
the latent is not a tiny JPEG: 4 learned channels,
not RGB, only meaningful inside the U-Net + VAE
The compression ratio is the source’s arithmetic. The four-channel view is a labelled teaching model — real VAE channels are learned mixtures — but the lesson holds: each channel is a smooth function of a whole 8×8 block, which is what makes 16,384 values enough to rebuild a photograph.
Two multipliers stack on top of the 48×. The first is the step count: a DDPM-era pixel sampler wanted ~50 steps, while a second-order latent sampler matches it in ~20–25 (the sampler chapter). The second is attention: transformer blocks pair every token with every other token, so cost grows with the square of the token count. A 512×512 image is 262,144 pixel tokens; a 64×64 latent is 4,096. Square those and the attention pair count falls from 68.7B to 16.8M — 4,096× fewer. That is the honest reason nobody trains high-resolution pixel-space transformers.
two independent multipliers, one image
value-updates DDIM-50 in pixels 50 × 786,432 = 39,321,600
DPM-25 in latents 25 × 16,384 = 409,600
ratio 39,321,600 ÷ 409,600 = 96×
attention pairs 262,144² = 68.7B
4,096² = 16.8M
ratio = 4,096×
The cost moves to the VAE, which runs twice per image (encode once, decode once) instead of 25 times. That is the trade in one sentence: pay a fixed compression bill, and stop paying the pixel bill at every single step.
Quick check
In a Stable Diffusion pipeline, which part runs in pixel space at full 512×512 resolution?
02
FIVE PIECES
An assembly line with five job descriptions.
Every Stable Diffusion pipeline — 1.5, SDXL, SD3, FLUX, the video models — is a variation on five pieces: a VAE, a text encoder, a U-Net, a scheduler and (optionally) a safety checker. Learn the five job descriptions and the model cards stop being alphabet soup.
Follow one generation through the line and the shapes tell the story. The prompt goes through a tokenizer and a text encoder into a 77 × 768 embedding — a sequence, not one vector, because the denoiser is going to attend to individual words. A random 4×64×64 latent — all 16,384 values of it — enters the U-Net together with the current timestep and that embedding. The U-Net predicts the noise; the scheduler uses the prediction to produce the next latent; the loop runs 20–50 times. Finally the VAE decodes the clean latent into a 512×512 image, and the optional safety checker decides whether you get to see it.
piece
in → out
job
Text encoder 77 tokens is CLIP's context window; the tokenizer pads or truncates every prompt to it.
“a village square, ghibli style” → 77 × 768
Turn the prompt into a sequence of token embeddings the denoiser can attend to.
U-Net ~860M parameters in SD 1.5; the source puts SDXL at ~2.6B and FLUX at ~12B, mostly in attention.
Translate between pixels and the latent space the diffusion model actually models. Encoder for img2img and training, decoder for every output.
Safety checker Fails closed by default in the classic pipeline: a flagged image comes back as a black square.
3 × 512 × 512 RGB → image or blank
Optional post-hoc filter for NSFW and illegal content, run on the decoded image (and sometimes on the prompt).
The order in that table is not the order of the data flow — the scheduler and the U-Net pass work back and forth, which is why the explorer draws them as a loop. And two of the five hold no trained weights of their own: the scheduler is a pure algorithm, and the safety checker is a separate classifier that runs on the output. When people talk about “the model”, they almost always mean the U-Net plus whatever encoder it was trained with.
The pipeline explorer
Five pieces, five jobs. Click any row to read the tensor shape crossing each edge, the role, and the one number worth remembering.
pick a piece
piece U-Net (860M params)
in 4 × 64 × 64 noisy latent + t + 77 × 768 text
out 4 × 64 × 64 predicted noise
role
Predict the noise in the latent at timestep t, with cross-attention to the text embedding at every resolution level.
detail
The same U-Net architecture as Lesson 07's segmenter, with transformer blocks (self-attention + cross-attention) inserted at every spatial level and a timestep embedding added to every block. Its last layer outputs 4 channels — latent noise, never an image.
number
~860M parameters in SD 1.5; the source puts SDXL at ~2.6B and FLUX at ~12B, mostly in attention.
The flow is not a straight line: the scheduler feeds the U-Net the next latent and the U-Net feeds the scheduler a prediction, once per step. The text encoder runs once per prompt; the VAE decoder runs once per image.
Quick check
You replace DDIM with DPM-Solver++ and drop the step count from 50 to 20. Which of the five pieces just changed its weights?
03
INSIDE THE LATENT
The latent is not a small picture. It is a different kind of object.
Four channels at 64×64 do not look like the image, save like the image, or average like the image. They are a point on a manifold the VAE learned, and the U-Net was trained to model that manifold. That geometric fact is the whole reason img2img and inpainting work — and the reason decoding the wrong latent produces garbage instead of noise.
The VAE is a variational autoencoder: its encoder does not output one point, it outputs a small Gaussian distribution, and a sample from it becomes the latent. Then a fixed constant rescales the raw latent to roughly unit variance — 0.18215 in SD 1.5/2.x, 0.13025 in SDXL. That constant is hardcoded in every pipeline, and getting it wrong is one of the classic silent bugs of DIY latent diffusion.
Encode, scale, decode — the VAE's entire jobpython
The encoder samples from a distribution, so an image maps to a neighbourhood of latents, not one exact point. The decode is lossy — fine textures come back slightly smoothed — but the structure is preserved, which is what img2img relies on.
Two consequences follow from latent geometry, and both are products you have used.
Img2img works because encoding is near-invertible: encode the input image, add a controlled amount of noise, run the normal denoising loop, decode. The structure survives in the latent and the prompt reshapes the content. The dial is strength — the fraction of the schedule you start at. With 30 steps, strength 0.6 means 18 real denoising steps; strength 1.0 means the latent is replaced by pure noise before the loop begins, so the input image contributes nothing at all.
Inpainting is img2img with a second mask-shaped input: the model predicts the whole latent every step, but after each step the untouched region is pasted back from the encoded original. White pixels in the mask get regenerated; black pixels are preserved exactly. Everything else in the pipeline is unchanged, which is why an inpainting checkpoint is just a U-Net that was additionally trained with masked latents.
Quick check
You encode a photo, add 60% of the schedule's noise, denoise the full schedule at strength 1.0, and decode. What does the pipeline see as its input?
04
GUIDANCE WITHOUT A CLASSIFIER
The prompt steers with one subtraction.
Cross-attention lets every patch of the latent look at every word of the prompt, at every level of the U-Net. Then classifier-free guidance amplifies the difference between “with the prompt” and “without it” — and that single knob is why a text-to-image model feels like it obeys you.
The text encoder turns the prompt into a 77 × 768 sequence of embeddings. Inside the U-Net, a cross-attention layer builds its queries from the latent patches and its keys and values from that text sequence: each of the 4,096 latent tokens asks which words matter for it. Because those layers exist at every resolution level, the prompt can influence coarse composition at 8×8 and fine detail at 64×64. Without cross-attention, text could only enter as one pooled sentence vector — and prompts would bias the image the way a colour tint biases a photo.
Conditioning is trained by dropping it. During training, the prompt is replaced by an empty embedding about 10% of the time, and the same weights learn to predict noise both with and without the condition. At inference you get two predictions from one model and combine them:
eps_cond the model's prediction with your prompt
eps_uncond the same model's prediction with an empty prompt
w the guidance scale (SD's classic default: 7.5)
eps = eps_uncond + w × (eps_cond − eps_uncond)
worked example eps_uncond = 0.20, eps_cond = 1.20 (the difference is 1.00)
w = 0 0.20 + 0 × 1.00 = 0.20 → unconditional: the prompt is ignored
w = 1 0.20 + 1 × 1.00 = 1.20 → plain conditional, no amplification
w = 7.5 0.20 + 7.5 × 1.00 = 7.70 → the conditional direction is multiplied 7.5×
The guidance playground
Raise the guidance scale and watch the guided distribution narrow toward the prompt: adherence climbs, diversity falls. The nine dots are fixed quantiles — they are a picture of how much variety nine samples would have at this scale.
w 7.5 · standard
guided mean 1.213 (conditional: 1.000)
guided sigma 0.181 (conditional: 0.450)
adherence 121.3%
diversity 18.1%
scalar example
eps_uncond 0.20
eps_cond 1.20
eps_guided 7.70 = 0.20 + 7.5 × 1.00
expected per the source's sweep
the classic SD default
Two forward passes per step is the price of CFG: the classic pipeline batches an empty prompt with your prompt so one U-Net call returns eps_u and eps_c together — which is why guidance doubles sampling time. The curves are a labelled teaching model; the rule they apply is exactly the one in the code.
Why the amplification works: the conditional prediction points from “unconditional” toward “matches the prompt”, and w scales that direction. Too small and the prompt barely moves the sample; the source calls guidance the reason text-to-image works at production quality at all. Too large and the sampler walks past the region the VAE can decode, which is what the burned colours and posterised edges at w = 15 are. The production band is 7–8.5, with 7.5 the number every tutorial quotes.
One implementation detail explains the cost curve: the classic pipeline does not run the U-Net twice. It batches an empty prompt and your prompt into one call of size two, so a single forward pass returns eps_uncond and eps_cond together — and every guidance step still costs two forward passes’ worth of compute. Classifier-free guidance roughly doubles sampling time, which is exactly why the sampler chapter’s step-count arithmetic matters.
Quick check
Why does a step with CFG cost roughly twice as much compute as a step without it?
05
FEWER STEPS
The same model can walk 50 steps or 20.
Training fixes a noise schedule a thousand steps long. Sampling does not have to visit every step: a scheduler chooses which timesteps to visit and how to turn each prediction into the next latent. It is the cheapest quality knob in the stack — and the one people leave at the default longest.
The DDPM training objective cares about every timestep, but the generation process is just an ODE (or SDE) to integrate backwards from noise to data. A first-order solver takes small, uniform steps; DDIM does exactly that and wants ~50 of them. A higher-order solver can reuse the previous prediction to take a smarter step, which is how DPM-Solver++ reaches comparable quality in 20–30. Distilled variants train a model or adapter to jump several steps at once, trading some quality and prompt fidelity for 1–4 step generation.
sampler
kind
typical steps
why you would pick it
DDIM
first-order deterministic
50
Deterministic, simple, skips timesteps without retraining. The 50-step baseline every other solver is compared against.
Euler ancestral
first-order stochastic
35
Adds fresh noise at each step, so seeds differ run to run; slightly more “creative” samples at 30–50 steps.
DPM-Solver++ 2M Karras
second-order deterministic
20
A higher-order ODE integrator: it reuses the previous prediction, so it converges in fewer steps. The source's production default in the mid-2020s.
LCM-LoRA / Turbo
distilled / consistency
4
A distilled adapter or model trained to jump several steps at once: 1–4 steps, at some cost to quality and prompt fidelity.
Swapping one is a one-line change, because scheduler state is decoupled from the weights: you can train on DDPM and sample with anything. The source puts it exactly that way — sometimes a swap fixes sample issues with no retraining at all.
Swap the sampler (no weights change)python
from diffusers import DPMSolverMultistepScheduler, EulerAncestralDiscreteScheduler
# keep the model's training schedule, take the sampler's inference recipe
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe.scheduler.config)
image = pipe(
prompt="a village square, ghibli style",
guidance_scale=7.5,
num_inference_steps=25,
generator=torch.Generator("cuda").manual_seed(42),
).images[0]
`from_config` reads the existing schedule's config (the betas the model was trained with) and builds the new sampler's schedule from it. Deterministic samplers reproduce a seed exactly; ancestral ones inject noise, so the same seed still varies.
The step count is where the wall clock lives. Take 130 ms as a stand-in for one 512² fp16 U-Net pass on a 2022-era consumer GPU, and remember that CFG doubles every step:
per step 1 U-Net pass 130 ms
with CFG (2 passes) 260 ms
50 steps 50 × 260 ms = 13.00 s (DDIM-50)
25 steps 25 × 260 ms = 6.50 s (DPM-Solver++ 2M)
20 steps 20 × 260 ms = 5.20 s (the source's production default)
4 steps 4 × 260 ms = 1.04 s (LCM / Turbo territory)
halving the steps halves the bill; the source's claim is that DPM-25 matches DDIM-50
Treat that 130 ms as a labelled stand-in, not a current benchmark: the real number depends on your card, precision, resolution and attention implementation (PyTorch SDPA, xformers, FlashAttention). The ratio is the durable part — four-step generation is an order of magnitude cheaper than fifty, and that is what makes interactive and batch workloads possible.
The sampler stepper
Pick a solver and a step budget. The thumbnails show the same latent walk at nine checkpoints; the curve shows why a second-order solver gets away with far fewer steps than DDIM.
solver DPM-Solver++ 2M Karras
kind second-order deterministic
steps 20 (advertised default)
quality 99.0% at 20 steps
DDIM compare 84.2% at the same budget
99.0% at 50 steps
per-step k 0.2057
wall clock 5.20 s with CFG · 2.60 s without
why it works
A higher-order ODE integrator: it reuses the previous prediction, so it converges in fewer steps. The source's production default in the mid-2020s.
A sampler is a recipe, not weights: nothing here retrains anything. Going below ~8 steps usually needs a distilled model or adapter — that is what the LCM row is.
06
FOUR PIPELINES
One base model, fourteen lines each.
Text-to-image, image-to-image, inpainting and ControlNet are not four models — they are four ways of feeding the same latent loop. Each one changes what enters the U-Net: nothing, an encoded image, an encoded image plus a mask, or an extra structural hint.
The source runs the whole lesson through diffusers rather than rebuilding the pieces — the VAE, text encoder, U-Net and scheduler are each topics of their own. What you are learning here is fluency with the production API: which pipeline class, which inputs, and which two or three numbers actually change the picture.
1 · Text-to-image — the baseline every other workflow extendspython
import torch
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16, # halves VRAM with no visible quality loss
).to("cuda")
image = pipe(
prompt="a dog riding a skateboard in tokyo, studio ghibli style",
guidance_scale=7.5, # the classic CFG default
num_inference_steps=25, # DPM-Solver++ quality ≈ DDIM at 50
generator=torch.Generator("cuda").manual_seed(42),
).images[0]
image.save("dog.png")
Pin the seed while you experiment. float16 halves VRAM; on newer accelerators bfloat16 has more range and fewer overflow surprises. Model ids change and checkpoints move — always read the model card for the sampler and steps it recommends.
from diffusers import StableDiffusionImg2ImgPipeline
from diffusers.utils import load_image
img2img = StableDiffusionImg2ImgPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16
).to("cuda")
init_image = load_image("dog.png").resize((512, 512))
out = img2img(
prompt="a dog riding a skateboard, oil painting",
image=init_image,
strength=0.6, # 0.0 unchanged · 1.0 fully regenerated
guidance_scale=7.5,
num_inference_steps=30,
).images[0]
strength is the fraction of the schedule you start at, so it also sets the real number of steps: 30 × 0.6 = 18. The 0.5–0.7 band is the standard style-transfer range — and the input structure survives only because the VAE's encode is near-invertible.
3 · Inpainting — img2img with a maskpython
from diffusers import StableDiffusionInpaintPipeline
from diffusers.utils import load_image
inpaint = StableDiffusionInpaintPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5", torch_dtype=torch.float16
).to("cuda")
image = load_image("dog.png").resize((512, 512))
mask = load_image("dog_mask.png").convert("L").resize((512, 512)) # white = regenerate
out = inpaint(
prompt="a cat",
image=image,
mask_image=mask,
guidance_scale=7.5,
num_inference_steps=30,
).images[0]
White pixels in the mask are regenerated; black pixels are preserved from the encoded original. Inpainting checkpoints are U-Nets additionally trained with masked latents, so the pipeline class is what changes from img2img — the base can be an inpainting checkpoint or, with the right weights, a normal one.
4 · ControlNet — structure as a second conditionpython
import torch
from diffusers import ControlNetModel, StableDiffusionControlNetPipeline
from diffusers.utils import load_image
controlnet = ControlNetModel.from_pretrained(
"lllyasviel/sd-controlnet-canny", torch_dtype=torch.float16
)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
controlnet=controlnet,
torch_dtype=torch.float16,
).to("cuda")
canny = load_image("dog_canny.png") # edges, depth, pose, scribble — one map per adapter
out = pipe(
prompt="a dog riding a skateboard, studio ghibli style",
image=canny,
controlnet_conditioning_scale=0.8, # how hard the structure pushes
guidance_scale=7.5,
num_inference_steps=25,
).images[0]
ControlNet is a separately trained adapter that clones the base U-Net's whole encoder half — every down block plus the middle block — and wires the copy back through zero-initialised convolutions, so it starts as a no-op and learns to steer. Different control types ship as different checkpoints (canny, depth, pose, segmentation); conditioning scale 0.6–1.0 is the usual band, and the exact adapter ids move with the ecosystem.
Which one for which job: no input structure and no constraint ⟶ text-to-image; an image whose composition you want to keep while the style changes ⟶ img2img at 0.4–0.7; a region to replace ⟶ inpainting; and a structure you care about pixel-for-pixel but do not want to inherit the colours of ⟶ ControlNet. The last one is the professional favourite because it separates structure from appearance: draw a rough pose or a wireframe, and let the prompt own everything else.
07
ADAPTERS, NOT RETRAINING
Fine-tune one percent. Keep the other ninety-nine frozen.
Full fine-tuning updates all 860,000,000 U-Net parameters and wants 20+ GB of VRAM. LoRA freezes every one of them and trains small rank-decomposition matrices inside the attention layers instead — minutes on one consumer GPU, and a file you can swap at inference.
LoRA (Hu et al., 2021) came out of language modelling and transferred to Stable Diffusion almost unchanged. For a weight matrix W of shape d_in × d_out, keep W frozen and learn two skinny matrices instead: A of shape d_in × r and B of shape r × d_out. The layer computes W + α·(A@B). A is initialised randomly, B at zero, so the adapter starts as an exact no-op and can only add to what the base already knows.
full fine-tune all 860,000,000 params update · 20+ GB VRAM · a new checkpoint (GBs)
LoRA base frozen, rank-r adapters trained
W + α·(A @ B) A: d_in × r B: r × d_out
rank r typically 4–32 (community SD adapters land in 4–16)
worked rank arithmetic (one attention projection)
d = 320 base 320² = 102,400
rank 4 adds 2·320·4 = 2,560 (2.5% of that matrix)
rank 16 adds 2·320·16 = 10,240 (10.0%)
d = 1280 base 1280² = 1,638,400
rank 4 adds 2·1280·4 = 10,240 (0.63%)
across the whole U-Net, adapters land at ~1–3% of the base:
1–3% of 860M ≈ 8.6M–25.8M weights → 17 MB–52 MB at fp16
the source's range: 10–50 MB adapters, trained in 10–60 minutes
The training loop is the diffusion objective from Lesson 10 with three fixed pieces: the VAE is frozen and only encodes, the text encoder is frozen and only embeds, and the U-Net is frozen except for the LoRA matrices injected into its attention layers. Only those receive gradient.
The LoRA training step (sketch — peft or diffusers.training run this in practice)python
for step, batch in enumerate(dataloader):
images, prompts = batch
latents = vae.encode(images).latent_dist.sample() * 0.18215# frozen VAE
t = torch.randint(0, num_train_timesteps, (batch_size,))
noise = torch.randn_like(latents)
noisy_latents = scheduler.add_noise(latents, noise, t)
text_emb = text_encoder(tokenizer(prompts)) # frozen text encoder
pred_noise = unet(noisy_latents, t, text_emb) # LoRA weights live here
loss = F.mse_loss(pred_noise, noise)
loss.backward() # only the LoRA matrices get gradients
optimizer.step()
Batch size 1 plus gradient checkpointing is what fits this in 8 GB. The learning rate is much higher than a full fine-tune's — the adapter is starting from zero, not from a pretrained solution.
At inference the adapter is a plugin, not a new model. load_lora_weights patches the attention layers; lora_scale dials its strength from 0 to 1; fuse_lora bakes it into the weights for speed, after which you must unfuse_lora before loading a different adapter. Several adapters can be loaded at once, each with its own weight — which is how a “style + character” stack is assembled.
One distinction worth keeping straight: LoRA changes how much of the model you train. DreamBooth is a different recipe about what you teach — a rare token bound to one subject, usually from 3–5 images, with class-prior preservation so the class (say, “dog”) does not get overwritten. In practice the two combine: DreamBooth-style data, LoRA-style parameters, which is how a subject adapter trains on a single consumer card.
The fine-tuning board
Set your dataset and your GPU and read the recipe: which parameters receive gradient, what artifact comes out, how long it takes, and what to watch for. Then the adapter-loading recipe at the bottom.
DATASET × VRAM → RECIPE
STYLE / CHARACTER · 20 IMAGES · 8 GBLoRA — a style or character adapter
Trains · the attention projections only, at rank 8 — the base stays frozen
Produces · a 10–50 MB adapter that loads next to any compatible checkpoint
steps 2,000 (images × 100, clamped to 800–6,000)
time 13.3 min at ~2.5 steps/s, batch 1, gradient checkpointing
rank 8
adapter ~0 MB fp16 (~2% of the 860M base as a stand-in)
Recipe · captions matter more than hyperparameters here: describe what is constant (the style) and vary what is not, or the adapter binds to the background too.
Watch · 20–100 images is the comfortable band. Below that, expect identity drift; far above it, a LoRA starts to need the training set to stay faithful — at which point test a full fine-tune.
mode
what trains
VRAM
artifact
data
Full fine-tune
all ~860M U-Net weights
20+ GB
GBs · a new base
thousands of images
LoRA
rank-decomposed attention adapters (~1–3% of weights)
6–8 GB
10–50 MB · loads on top
10–100 images
DreamBooth
the whole U-Net (or LoRA) on one subject
full-model VRAM, or 8 GB as DreamBooth+LoRA
a named subject
3–5 images
attention width d
rank r
base weights d²
LoRA adds 2·d·r
share
320
4
102,400
2,560
2.50%
320
16
102,400
10,240
10.00%
1280
4
1,638,400
10,240
0.63%
1280
16
1,638,400
40,960
2.50%
Shared across the U-Net, the attention adapters land at roughly 1–3% of the 860,000,000 base parameters — 10–50 MB at fp16, which is why adapters are the community’s distribution format.
Load a LoRA adapter (and stack a second)python
import torch
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16,
).to("cuda")
# 1. load a community adapter and try a scale
pipe.load_lora_weights("sayakpaul/sd-lora-ghibli", adapter_name="ghibli")
pipe.set_adapters("ghibli", adapter_weights=0.8)
image = pipe("a village square, ghibli style").images[0]
# 2. stack a second adapter (style + character)
pipe.load_lora_weights("user/character-lora", adapter_name="character")
pipe.set_adapters(["ghibli", "character"], adapter_weights=[0.8, 0.6])
# 3. fuse bakes the adapter into the weights: faster, but no longer swappable
pipe.fuse_lora(lora_scale=0.8)
pipe.unfuse_lora() # call this before loading a different adapter
`set_adapters` takes weights, so 0.0–1.0 is a free strength dial; the exact API surface moves between diffusers releases — check the docs page linked at the end of the lesson.
GPU VRAM
images 20
vram 8 GB
recommended LoRA — a style or character adapter
mode style / character
rank 8
steps 2,000
time 13.3 min (stand-in: 2.5 steps/s)
adapter ~0 MB fp16
hard numbers from the source
full fine-tune 20+ GB VRAM · 860M params updated
LoRA 6–8 GB with batch 1 + gradient checkpointing
the base stays frozen in both LoRA rows
recipe
captions matter more than hyperparameters here: describe what is constant (the style) and vary what is not, or the adapter binds to the background too.
The dataset × VRAM grid is a teaching model assembled from the source’s numbers (20+ GB for a full fine-tune, 8 GB for LoRA with checkpointing, 10–50 MB adapters, minutes-to-an-hour runs) plus the community rule of thumb for step counts. Your card, resolution and precision will move the estimate — the decision structure is the part to keep.
08
CHECK YOURSELF
Five questions. Then the terms worth keeping.
Answer before you look. The latent-space question and the CFG question are the two that separate “I ran the notebook” from “I can choose the model, the sampler and the guidance for the next project.”
0 / 5 answered · 0 correct
01Why does Stable Diffusion run its DDPM in a 4×64×64 latent space rather than directly on 3×512×512 pixel images?
02What does classifier-free guidance (CFG) do at inference?
03You swap SD's default scheduler for DPM-Solver++ 2M Karras and reduce num_inference_steps from 50 to 20. What is the expected result?
04Why is LoRA fine-tuning popular for Stable Diffusion instead of full fine-tuning?
05You generate the same prompt with guidance_scale=15 and see oversaturated colours and burned-in artefacts. What is going on?
Key terms, demystified
Click a card to swap the lazy description for what it actually means.
Exercises from the lesson
Three experiments with real numbers: sweep the guidance scale until the image burns, sweep img2img strength until the subject drifts, and train a subject LoRA and report the rank and step count that worked. Try first; a worked answer is one click away.
Generate the same prompt with guidance_scale in [1, 3, 5, 7.5, 10, 15]. Describe how the image changes. At what guidance value do artefacts appear?Show one worked answer
The experiment is a one-line loop: fix the seed and the scheduler, and vary only the scale. Expected shape, from the CFG arithmetic and the source's sweep: w = 1 is plain conditional with no amplification (unconditional would be w = 0), so the image is plausible but only loosely related to the prompt; w = 3 gives creative, loosely related images; w = 5 tightens adherence while keeping variety; w = 7.5 — SD's classic default — is the production band, where the prompt usually wins. At w = 10 adherence is strong and colours begin to over-saturate; by w = 15 the image is typically burned: posterised edges, clipped highlights, and a look the source calls heavy artefacts. The lab's 1-D model shows the mechanism: at w = 7.5 the guided distribution has mean 1.21 and σ 0.181, at w = 15 mean 1.233 and σ 0.130 — the distribution keeps narrowing toward the conditional mean, so the sampler keeps walking further from the unconditional region the decoder knows, and the diversity proxy (σ_guided against the unconditional's σ = 1) falls from 0.18 to 0.13 of that spread. The honest calibration step: the exact onset moves with the model, the scheduler, and whether it supports guidance scheduling, so run the sweep on your checkpoint and record the first w where the colours clip — that is your ceiling, usually somewhere in 9–12.
Take any real photograph and run it through StableDiffusionImg2ImgPipeline at strength in [0.2, 0.4, 0.6, 0.8, 1.0]. Which strength preserves composition while changing style? Why does 1.0 ignore the input entirely?Show one worked answer
With `num_inference_steps=30`, strength s starts the schedule at step 30·s: 6 steps at 0.2, 12 at 0.4, 18 at 0.6, 24 at 0.8, 30 at 1.0. The composition survives in proportion to how little noise was added: 0.2 changes palette and texture while the geometry is locked (and often barely differs from the prompt's style); 0.4–0.6 is the style-transfer band the source calls standard, with 0.6 the point where a strong style prompt visibly reshapes the subject; 0.8 keeps only the large-scale layout; and 1.0 replaces the encoded image with pure noise from the start, so the init image contributes nothing — the pipeline is text-to-image with extra steps. Two consequences worth checking: the effective number of denoising steps shrinks with strength, so img2img at 0.2 is fast and at 1.0 costs a full run; and the mask-free structure survives only because the VAE's encode is near-invertible, which is the latent-geometry point this lesson makes. Report the exact strength where the subject identity starts to drift — that threshold is what a pipeline needs to document.
Train a LoRA on 10–20 images of a single subject (a pet, a logo, a character) and generate novel scenes with that subject in them. Report the LoRA rank and training steps that produced the best identity preservation without overfitting to the input images.Show one worked answer
A workable configuration from the source's recipe: 10–20 captioned images, rank 8–16, batch size 1, gradient checkpointing, fp16, 512×512, AdamW at a learning rate well above a full fine-tune's (the adapter starts from zero, not from a pretrained solution), and roughly 1,000–2,000 steps — the board in this lesson computes 20 images × 100 = 2,000 steps, about 13 minutes at 2.5 steps/s. Why those ranks: rank 8 on a 320-wide projection adds 2·320·8 = 5,120 weights to a 102,400-weight matrix (5%), and across the U-Net the whole adapter stays around 1–3% of the 860M base — a 10–50 MB fp16 file. Overfitting shows up as the subject appearing in every prompt, backgrounds from the dataset leaking into new scenes, or the subject ignoring the prompt's composition. Test with prompts the dataset never contained (holding a balloon, underwater, at night) and compare against the untouched base as the control. If identity is weak, raise rank before steps; if the subject invades unrelated prompts, lower the rank, drop steps, or lower the LoRA scale to 0.6–0.8 at inference. For 3–5 images instead of 10–20, switch to DreamBooth with class-prior preservation — that is the subject-matter recipe, and most community runs pair it with LoRA for the VRAM budget.
Terms this lesson borrows from later lessons (or outside)
You do not need to master these here. Each one gets a proper treatment in its own lesson; the one-line meaning is enough to keep reading. Orange dotted underlines in the prose point back to this list.
DDPM training and sampling (Phase 4, Lesson 10) — The forward noising process q(x_t | x_0), the noise-prediction MSE objective, and the DDIM sampler SD inherits. Stable Diffusion changes where the diffusion happens — the latent space — not what it is; Lesson 10's TinyUNet is the same idea at 32×32.
U-Net encoder–decoder with skip connections (Phase 4, Lesson 07) — SD's denoiser is a much larger U-Net: the same down/up path with skips, plus transformer blocks at every level and a timestep embedding. Knowing which tensor a block consumes is what makes the shape table in this lesson legible.
Self-attention (Phase 7, Lesson 02) — Cross-attention is attention with two different sources: queries from the latent patches, keys and values from the 77 text tokens. The scaled dot-product is unchanged; only where Q comes from is new.
Transfer learning and freezing (Phase 4, Lesson 05) — LoRA is the same move as a frozen backbone with a trained head, one level deeper: freeze 860M weights, train a small add-on, and judge the run by comparing against the untouched base — the probe-floor ritual in adapter form.
Optimizers (Phase 3, Lesson 06) — The LoRA training loop uses AdamW over the adapter parameters only — the source points at `peft` or `diffusers.training`. A low-rank update wants a much larger learning rate than a full fine-tune, because it is starting from zero rather than from a pretrained solution.
Numerical stability (Phase 1, Lesson 13) — SD runs in float16, where the range spans ~1e-5 to ~65,000: latents, predictions and gradients all dance near the limits. That is why bfloat16 is preferred on newer accelerators and why NaN losses in diffusion fine-tunes are usually a precision problem, not a data problem.
KEEP GOING
A picture is a start. Practice is the rest.
This lesson is a port of an open course. Everything here traces back to it — and the next step is running the code yourself.
Lesson text adapted from AI Engineering from Scratch (Phase 04, Lesson 11) and the Math Foundations Notebook reference build. The five labs (the canvas 48× latent compressor, the clickable pipeline explorer, the CFG guidance playground, the sampler stepper, and the dataset × VRAM fine-tuning board) are original to this page, as are the two-multiplier sampling budget (DDIM-50 in pixels 39,321,600 value-updates versus DPM-25 in latents 409,600, a 96× ratio alongside the 48× per-step compression) and the attention-pair arithmetic (262,144² = 68.7B versus 4,096² = 16.8M, 4,096× fewer), the CFG worked example (eps_uncond 0.20 + 7.5 × 1.00 = 7.70) and the two-passes-per-step cost note, the guided-distribution walk (σ 0.45 → 0.181 at w = 7.5 → 0.130 at w = 15), the VAE scale factors (0.18215 for SD 1.5, 0.13025 for SDXL), the img2img step arithmetic (30 × 0.6 = 18 real steps; strength 1.0 starts from pure noise), the per-step wall-clock stand-in (130 ms × 2 passes = 6.5 s at 50 steps, 2.6 s at 20, 0.52 s at 4), the LoRA rank arithmetic at d = 320 and d = 1280 with the ~1–3%-of-860M → 17–52 MB fp16 adapter check, and the DreamBooth-versus-LoRA distinction. Model names, sampler defaults and licences are dated snapshots from the 2022–2024 ecosystem: this page presents them as examples with links to the authoritative papers, docs and model cards rather than as current specifications. The canvas scenes, the latent channel model, the guided-distribution model, the solver convergence model, the training-time estimate and the VRAM decision board are labelled teaching models, because a diffusion run cannot be replayed in a canvas; every other number shown is computed live by the labs or verified by hand in the prose.