TAI BUI
Back to curriculum
BuildPython

ControlNet, LoRA & Conditioning

Text alone is a clumsy control signal. ControlNet lets you clone a pretrained diffusion model and steer it with a depth map, pose skeleton, scribble, or edge image. LoRA lets you fine-tune a 2B-parameter model by training 10 million parameters. Together they turned Stable Diffusion from a toy into a standard 2026 image pipeline.

TIME: ~75 minutesPREREQUISITES: Phase 8 · 07 (Latent Diffusion), Phase 10 (LLMs from Scratch, for LoRA foundation)

Learning Objectives

  • Explain how ControlNet adds spatial conditioning to a frozen diffusion backbone through a cloned encoder and zero-convolution connections.
  • Write the LoRA update W + alpha * B @ A and compare its parameter count to a full d x d fine-tune.
  • Choose among ControlNet, LoRA, IP-Adapter, Textual Inversion, DreamBooth, and T2I-Adapter for a given control problem.
  • Avoid the common stacking failures: over-scaled LoRAs, conflicting ControlNet weights, and LoRAs loaded on the wrong base.

The Problem

A prompt like "a woman in a red dress walking a dog on a busy street" gives the model no information about where the dog is, what pose the woman is in, or the perspective of the street. Text pins down about 10% of what you need to specify an image. The rest is visual and cannot be described efficiently in words.

Training a new conditional model from scratch for each signal (pose, depth, canny, segmentation) is prohibitive. The alternative is to keep the 2.6B-param SDXL backbone frozen, attach a small side-network that reads the conditioning, and let it nudge the backbone's intermediate features. That is ControlNet.

Teaching the model new concepts (your face, your product, your style) runs into the same cost problem, because retraining the full model is too expensive. You want a delta about 100x smaller. That is LoRA: low-rank adapters that plug into existing attention weights.

ControlNet, LoRA, and text together make up the 2026 practitioner's toolkit. Most production image pipelines layer 2-5 LoRAs, 1-3 ControlNets, and an IP-Adapter on top of an SDXL / SD3 / Flux base.

Warm-up0/3
1. Fully retraining a large image model for every new conditioning signal (pose, depth, edges) would be impractical mainly because...Warm-up
Three small pictures, a pose skeleton, a depth map and an edge outline, each with an arrow into its own box labelled image model, retrained for pose, for depth and for edges.
Schematic icons; no costs or sizes are shown.
2. A prompt says 'a dog on a busy street'. Which detail can text alone least precisely pin down?Warm-up
A text box reading 'a dog on a busy street' with an arrow to an outline of a street frame with building blocks, a road with lane marks and two cars, and no dog drawn in it.
Typed schematic; not a generated image.
3. A d x d matrix is written as the product of a d x r matrix and an r x d matrix, with r much smaller than d. What is the highest rank that product can have?Warm-up
A 6 by 2 grid labelled d x r, times a 2 by 6 grid labelled r x d, equals a 6 by 6 grid labelled d x d, with d = 6 and r = 2 as illustrative sizes.
Illustrative sizes d = 6, r = 2.

The Concept

ControlNet clones the encoder; LoRA adds low-rank deltas

ControlNet (Zhang et al., 2023)

ControlNet is a trainable clone of a pretrained Stable Diffusion U-Net's encoder that accepts an extra conditioning input (edges, depth, pose) and feeds its features into the original decoder. To build one, take a pretrained SD, clone the encoder half of the U-Net, and freeze the original. Train the clone so it accepts the conditioning input. Connect it back to the decoder half of the original through zero-convolution skip connections: 1×1 convs initialized to zero, so they start as a no-op and learn a delta.

SD U-Net decoder:   ... ← orig_enc_features + zero_conv(controlnet_enc(condition))

Because of the zero-conv init, ControlNet starts as identity and does no harm even before training. Train it on about 1M (prompt, condition, image) triples with the standard diffusion loss.

Per-modality ControlNets ship as small side models (~360M for SDXL, ~70M for SD 1.5). You can compose them at inference:

features += weight_a * control_a(depth) + weight_b * control_b(pose)

LoRA (Hu et al., 2021)

LoRA (low-rank adaptation) freezes a pretrained weight matrix and learns a low-rank delta that is added on top of it. For any linear layer W ∈ R^{d×d} in the model, add:

W' = W + ΔW,  ΔW = B @ A,  A ∈ R^{r×d},  B ∈ R^{d×r}

with r << d. Initialize B to zeros (as main.py does) and ΔW starts at zero, so a fresh LoRA is also a no-op, the same trick as zero-conv. Rank 4-16 is standard for attention, rank 64-128 for heavy fine-tunes. The new parameter count is 2 · d · r instead of d². For SDXL attention with d=640 and r=16, that is 20k params per adapter instead of 410k, a 20x reduction. Across the whole model, a LoRA is usually 20-200MB against a 5GB base.

At inference you can scale the LoRA: W' = W + α · B @ A. α = 0.5-1.5 is normal. Multiple LoRAs stack additively, though they interact in non-linear ways.

IP-Adapter (Ye et al., 2023)

An IP-Adapter is a tiny adapter that accepts an image as conditioning alongside text. It uses the CLIP image encoder to produce image tokens and injects them into cross-attention next to the text tokens. It costs ~20MB per base model and lets you generate an image in the style of a reference without a LoRA.

Check: The Concept0/13
1. What is a ControlNet?Define
An edge map of a simple house and a person, and the prompt 'a woman in a red dress walking a dog on a busy street', both feeding a box labelled SDXL plus ControlNet (ControlNet-Canny), with an arrow to an output image tile.
The output tile is a schematic, not a generated image.
2. What is a zero convolution?Define
A diagram with an original encoder feeding an original decoder, and a condition input going through a cloned encoder and a box named zero_conv whose output is added into the decoder, with the lesson's formula line below.
Schematic from the lesson's Concept section.
3. What is LoRA (low-rank adaptation)?Define
A bar for a base checkpoint of about 5 GB and a much shorter bar for a LoRA file of 20 to 200 MB, plus the line pipe.load_lora_weights('user/style-lora') used to load one.
Sizes and code line are from the lesson.
4. In LoRA, what does the rank r control?Define
A bar chart of the parameters in one LoRA adapter on a layer with d = 640 for ranks r = 4, 16 and 64: 5120, 20480 and 81920.
Computed here as 2 * d * r.
5. In W' = W + α · B @ A, what is α?Define
The equation W' = W + alpha times B @ A, with boxes below marking W as the frozen base, B @ A as the low-rank delta and alpha as the symbol in question, plus the note that normal values are 0.5 to 1.5.
Normal range is from the lesson.
6. What does IP-Adapter use to turn a reference image into a conditioning signal?Define
A reference image tile going into a dashed box marked with a question mark (image to signal), then into a diffusion model box that also receives a text prompt, producing an output image tile.
Schematic; the dashed box is the unknown the question asks about. Size is from the lesson.
7. Why is a ControlNet's output identical to the base model's before any training?Explain
Two identical picture tiles labelled base model alone and base plus new untrained ControlNet with an equals sign between them, with a note that the tiles are one schematic picture shown twice.
Schematic; not real generated images.
8. Why does ControlNet train a clone of the encoder while the original U-Net stays frozen?Explain
A diagram with a frozen original encoder feeding the original decoder, and a condition input feeding a trainable cloned encoder whose output passes through zero_conv and is added to the decoder features.
Schematic from the lesson's Concept section.
9. Why is B initialized to zeros in a fresh LoRA?Explain
A 6 by 1 column of zeros labelled B, times a 1 by 6 row of small random values labelled A, equals a 6 by 6 grid of question marks labelled delta W = B @ A.
A values are from running main.py's initialization; the delta grid is left blank.
10. What does the shorthand 'ControlNet is spatial, LoRA is semantic' mean?Explain
The shorthand 'ControlNet is spatial, LoRA is semantic' above two input panels: a pose skeleton for a figure, and about 30 curated brand images shown as placeholder tiles.
Inputs are from the lesson's Use It table; tiles are placeholders.
11. For an SDXL attention layer with d=640 and LoRA rank r=16, how many new parameters does LoRA add per layer, versus a full d-squared fine-tune?Apply
To-scale shapes for one SDXL attention layer: a large 640 by 640 square W (frozen), a very thin 640 by 16 strip B and a very thin 16 by 640 strip A.
Shapes drawn to scale; no counts are shown.
12. A LoRA is attached to a layer with d=1024 using rank r=8. How many parameters does it add, compared with a full d x d update?Apply
To-scale shapes for a layer with d = 1024 and r = 8: a large 1024 by 1024 square for a full update, and thin strips B (1024 by 8) and A (8 by 1024).
Shapes drawn to scale; no counts are shown.
13. You attach a new side branch to a frozen model, and the first generations look nothing like the base model's output. Which design choice was most likely skipped?Analyze
A frozen base model and a new side branch reading a condition, summed together, giving a first generation tile that differs from the base model's output tile.
Schematic; tiles are placeholders.

Composability matrix

ToolWhat it controlsSizeWhen to use
ControlNetSpatial structure (pose, depth, edges)70-360MBExact layout, composition
LoRAStyle, subject, concept20-200MBPersonalization, style
IP-AdapterStyle or subject from reference image20MBNo text can describe the look
Textual InversionSingle concept as a new token10KBLegacy, mostly replaced by LoRA
DreamBoothFull fine-tune on a subject2-5GBStrong identity, high compute
T2I-AdapterLighter ControlNet alternative70MBEdge devices, inference budget

ControlNet ≈ spatial, LoRA ≈ semantic. Use both.

Check: Composability matrix0/6
1. What is DreamBooth?Define
A 5 by 6 grid of thirty placeholder photo tiles labelled about 30 images of one subject, with an arrow to a box reading a model file of 2 to 5 GB, strong identity, high compute.
Tiles are placeholders; counts and sizes are from the lesson.
2. What does Textual Inversion train?Define
A typed example prompt 'a photo of <my-concept> in a garden' with the placeholder token highlighted, and two boxes: what training produces, a file of about 10 KB, and the lesson's note legacy, mostly replaced by LoRA.
Typed illustration; the token name is a placeholder.
3. Which pairing of tool and size or use is correct?Explain
A table with the rows DreamBooth, Textual Inversion, IP-Adapter and T2I-Adapter, and question marks in the size and use columns.
Cells are intentionally blank.
4. Why is Textual Inversion described as legacy and mostly replaced by LoRA?Explain
Two rows of the lesson's tool table: Textual Inversion, 10KB, legacy, mostly replaced by LoRA; and LoRA, 20-200MB, personalization and style.
Rows are from the lesson.
5. When does an IP-Adapter let you skip training a LoRA?Explain
Two paths: A uses about 30 curated images to train a LoRA of 20-200MB; B takes one reference image plus a text prompt into an IP-Adapter of 20MB.
Counts and sizes are from the lesson; tiles are placeholders.
6. An edge device needs edge-map-guided generation under a tight inference budget. Which tool fits best?Design
An edge map going into a box labelled edge device with a tight inference budget, and producing a guided image tile.
Schematic of the stem's need; tile is a placeholder.

Build It

code/main.py simulates the two mechanisms in pure Python:

  1. LoRA. Take a pretrained linear layer W and freeze it. Train a low-rank B @ A so that W + BA matches a target layer whose difference from W is a rank-1 matrix. The script uses 6×6 matrices, tries r = 1, 2, 4, and prints the residual MSE for each. All three print 0.00000, so r = 1 is already enough for a rank-1 correction (the script prints the residual but does not assert it).

  2. ControlNet-lite. A "frozen base" predictor plus a "side network" that reads an extra signal. A learnable scalar gate, initialized to zero (our version of zero-conv), scales the side network's output. This part is scalar. Train and watch the gate and the side weight move together toward the target.

Step 1: LoRA math

The snippet below is illustrative pseudocode. The real code is lora_forward, which computes (W + alpha * B @ A) @ x with a plain matrix-vector helper.

python
def lora(W, A, B, x, alpha=1.0):
    # W is frozen; A, B are the trainable low-rank factors.
    return [W[i][j] * x[j] for i, j in ...] + alpha * (B @ (A @ x))

Step 2: zero-init side network

python
side_out = control_net(x, condition)
gated = gate * side_out  # gate initialized to 0
h = base(x) + gated

At step 0 the output is identical to base, so the branch cannot disturb it before training starts. w_side starts small but nonzero, so gate gets a gradient immediately, while w_side gets none until gate has left zero. After that they climb together toward a product near the 0.7 in the target. The printout shows only steps 100, 300, 500, and 700, and by step 100 the gate is already near 0.8, so print the first few steps yourself to see the ramp.

Check: Build It0/5
1. In the ControlNet-lite script, why does w_side start small but nonzero while the gate starts at exactly zero?Explain
An excerpt of main.py's controlnet_toy: w_side drawn from a Gaussian with std 0.1, gate set to 0.0, the prediction x + gate * w_side * c, and the two gradient lines for gate and w_side with their updates.
Excerpt copied from the lesson's main.py (blank lines and the middle of the loop omitted).
2. A 1x1 layer has W=2. A rank-1 LoRA has B=0.5 and A=1, and alpha is 2. What does (W + alpha * B @ A) @ x return for x=3?Apply
Five boxes giving W = 2, B = 0.5, A = 1, alpha = 2 and x = 3, above the expression (W + alpha * B @ A) @ x = ?.
Values are from the question.
3. In the zero-init side network, gate is 0, base(x) is 3.0 and the side network returns 5.0 for the same input. What is h = base(x) + gate * side_out?Apply
A diagram: base(x) returns 3.0 and side_out returns 5.0, side_out is multiplied by a gate equal to 0, and the product is added to base(x) to give h, with the formula h = base(x) + gate * side_out.
Values are from the question; the formula is from the lesson.
4. The script's target delta is rank-1 and it tries r = 1, 2 and 4. If the target were a rank-2 delta instead, which ranks would you expect to reach a residual near zero?Apply
The text 0.5 * u1 v1^T + 0.5 * u2 v2^T for a 6 by 6 target delta, and three factor pairs B @ A drawn for r = 1, 2 and 4 with shapes 6x1 and 1x6, 6x2 and 2x6, 6x4 and 4x6.
Schematic of the setup; no results are shown.
5. You train a LoRA with r=1, but the real weight change you want to learn has rank 2, and the residual error stays high. What is the most likely fix?Analyze
The text of a rank-2 target change as a sum of two outer products, and a box for a LoRA with r = 1 (B 6x1, A 1x6) pointing to a box saying the residual error stays high.
Schematic of the situation in the question.

Pitfalls

  • Over-scaling LoRAs. α = 2 or α = 3 is a common "make it stronger" hack, and it produces over-stylized or broken outputs. Keep α ≤ 1.5.
  • ControlNet weight conflict. A Pose ControlNet at weight 1.0 plus a Depth ControlNet at weight 1.0 usually overshoots. A sum of weights around 1.0 is a safe default.
  • LoRA on the wrong base. SDXL LoRAs silently no-op on SD 1.5 because the attention dimensions do not match. Diffusers warns about this in 0.30+.
  • Textual Inversion drift. Tokens trained on one checkpoint drift badly on another. LoRA is more portable.
  • LoRA weight-merging and storage. You can bake a LoRA into the base model weights for faster inference (no runtime addition), but then you lose the ability to scale α at runtime. Keep both versions.
Check: Pitfalls0/5
1. What happens when you set LoRA's α to 2 or 3 to 'make it stronger'?Explain
A bar chart of the effective weight of a toy 1 by 1 layer with W = 2 and B @ A = 0.5 for alpha 0.5, 1, 1.5, 2 and 3: 2.25, 2.5, 2.75, 3.0 and 3.5.
Toy arithmetic computed here, not a generated image.
2. A Pose ControlNet and a Depth ControlNet are active together. Which weights are a safe default?Apply
The lesson's composition line features += weight_a * control_a(depth) + weight_b * control_b(pose), with a Depth ControlNet box and a Pose ControlNet box whose weights are marked with question marks.
Formula is from the lesson.
3. A colleague loads an SDXL-trained LoRA on an SD 1.5 checkpoint. What do they see?Analyze
A box for a LoRA trained on SDXL with an arrow labelled load into a box for an SD 1.5 checkpoint, followed by the text what do they see?.
Schematic of the setup.
4. Two LoRAs at α = 1 each give a result neither shows alone. What explains this?Analyze
The formula W' = W + 1 * B1 @ A1 + 1 * B2 @ A2 above two boxes, LoRA 1 and LoRA 2 each at alpha 1, sitting on a base model box.
Schematic of the stem's setup.
5. A style LoRA was baked into the base weights, and the style now looks too strong. What is the situation?Analyze
A style LoRA file is baked into the base weights, and an output tile labelled style looks too strong.
Schematic; tile is a placeholder.

Use It

Goal2026 pipeline
Reproduce a brand's art styleLoRA trained on ~30 curated images at rank 32
Put my face in a generated imageDreamBooth or LoRA + IP-Adapter-FaceID
Specific pose + promptControlNet-Openpose + SDXL + text
Depth-aware compositionControlNet-Depth + SD3
Reference + promptIP-Adapter + text
Exact layoutControlNet-Scribble or ControlNet-Canny
Background replaceControlNet-Seg + Inpainting (Lesson 09)
Fast 1-step styleLCM-LoRA on SDXL-Turbo
Check: Use It0/4
1. You need to replace the background of an image. Which pipeline fits?Apply
A schematic photo of a person standing on a street, and beside it a second panel with the same person and the label new background: ?.
Typed schematic; not real images.
2. Which pipeline fits 'Depth-aware composition'?Apply
A grayscale depth map of horizontal bands from dark (far) to light (near) beside a schematic scene of a hill and a sun-like disc, under the heading Goal: depth-aware composition.
Typed schematic; not real images.
3. An agency needs a fixed pose from a reference skeleton plus its brand's art style, and wants to change style strength per request. Which setup fits?Design
A fixed pose skeleton, a brand art style shown as three placeholder tiles, and a requests box listing three requests whose style strengths a1, a2 and a3 differ.
Schematic; strengths are unspecified symbols.
4. A studio wants exact layout from a sketch, a reference photo for the look that words cannot describe, and its brand style, all on SDXL. Which stack fits?Design
Three input boxes, a sketch for exact layout, a reference photo for a look words cannot describe and a brand style, all feeding an SDXL base model and producing an output tile.
Schematic of the stem; tile is a placeholder.

Ship It

Save outputs/skill-sd-toolkit-composer.md. The skill takes a task (input assets: prompt, optional reference image, optional pose, optional depth, optional scribble) and outputs the tool stack, weights, and a reproducible seed protocol.

Author's Note

I'd run main.py before touching diffusers and read the ControlNet-lite rows with the zero start in mind. The gate begins at 0, so at step 0 the output equals the base. By the first printed row (step 100) the gate and w_side have both left zero and are moving together toward a product near the 0.7 in the target. The printout skips the early steps, so I'd print the first few myself to see the ramp. Starting a new side branch at zero is a habit I'd keep.

My bet is that rank is the part of the LoRA half that carries furthest. The target delta in main.py is rank-1, and r of 1, 2, and 4 all fit it, with the residual printing as 0.00000 each time. The takeaway line says rank has to be at least the rank of the true delta, and that is the first thing I'd check on a LoRA that underfits, before touching α.

When stacking, I'd keep the weights modest and keep LoRAs unmerged. Merging buys ~3-5% per-step speed and costs you runtime control of α. Each active ControlNet costs roughly 1.5× per step, so if latency is the problem, I'd look at the ControlNets before I merged anything.

Exercises

  1. Easy. In code/main.py, the target delta is rank-1 and the script tries r in [1, 2, 4]. Change the delta to a sum of two outer products (rank 2) and rerun. At what rank does the residual MSE reach about zero?
  2. Medium. Train two separate LoRAs on two target transforms. Load them together and show their additive interaction. When does the interaction break linearity?
  3. Hard. Use diffusers to stack: SDXL-base + Canny-ControlNet (weight 0.8) + a style LoRA (α 0.8) + IP-Adapter (weight 0.6). Measure FID-vs-prompt-adherence trade-off as the stack weights vary.

Key Terms

TermWhat people sayWhat it actually means
ControlNet"Spatial control"Cloned encoder + zero-conv skips; reads a conditioning image.
Zero convolution"Starts as identity"1×1 conv initialized to zero; ControlNet starts as no-op.
LoRA"Low-rank adapter"W + B @ A, r << d; 100x fewer params than a full fine-tune.
rank r"The knob"LoRA compression; 4-16 typical, 64+ for heavy personalization.
α"LoRA strength"Runtime scaling of the LoRA delta.
IP-Adapter"Reference image"Small image-conditioning adapter via CLIP-image tokens.
DreamBooth"Full subject fine-tune"Train the full model on ~30 images of a subject.
Textual Inversion"New token"Learn a new word embedding only; legacy, mostly replaced.

Production note: LoRA swaps, ControlNet lanes, multi-tenant serving

A real text-to-image SaaS serves hundreds of LoRAs and a dozen ControlNets over the same base checkpoint. The serving problem looks a lot like LLM multi-tenancy (the production literature covers the LLM case under continuous batching and LoRAX / S-LoRA):

  • Hot-swap LoRAs, don't merge. Merging W' = W + α·B·A into the base gives ~3-5% faster per-step inference but freezes α and the base. Keep LoRAs hot in VRAM as rank-r deltas. Diffusers exposes pipe.load_lora_weights() plus pipe.set_adapters([...], adapter_weights=[...]) for per-request activation. A swap costs the 2 · d · r · num_layers weights: MB-scale, sub-second.
  • ControlNet as a second attention lane. The cloned encoder runs in parallel with the base. Two ControlNets at weight 1.0 each mean two extra forward passes per step, not one merged pass, and batch-size headroom drops quadratically. Budget ~1.5× step cost per active ControlNet.
  • Quantize the LoRAs too. If you quantized the base (see Lesson 07, Flux on 8GB), the LoRA delta also quantizes cleanly to 8-bit or 4-bit. QLoRA-style loading lets you stack 5-10 LoRAs on top of a 4-bit Flux base without blowing memory.

Flux-specific: Niels' Flux-on-8GB notebook quantizes the base to 4-bit, and stacking a style LoRA (pipe.load_lora_weights("user/style-lora")) on that quantized base at weight_name="pytorch_lora_weights.safetensors" still works. This is the recipe most SaaS agencies ship in 2026.

Check: Production note: LoRA swaps, ControlNet lanes, multi-tenant serving0/5
1. What is the trade-off of merging a LoRA into the base model instead of keeping it as a hot-swappable delta?Explain
Two setups: a hot-swappable delta with base weights W plus alpha times B @ A kept separate and added when a step runs, versus one merged matrix W' equal to W + alpha times B @ A computed once and saved.
Schematic of the two setups only.
2. Roughly how much does each active ControlNet add to per-step cost, and why?Explain
A diagram of one denoising step: a base model forward pass and two ControlNet lanes running in parallel, all feeding one features box and then the next step.
Schematic of the lesson's 'second attention lane' description; no costs drawn.
3. What happens when you stack LoRAs on a 4-bit quantized base?Explain
A solid box for a Flux base model quantized to 4-bit with four dashed placeholder LoRA chips above it and the heading 'LoRAs to stack on the quantized base: how many fit?'.
Schematic; the chip count is a placeholder.
4. A stack with three ControlNets and a style LoRA is too slow per step. Where should you look first to cut latency?Analyze
A stack: base model with a style LoRA, plus three separate ControlNet boxes, all feeding one denoising step.
Schematic of the stem's stack; no timings.
5. A service hosts hundreds of customer LoRAs on one SDXL base, and each request picks its own LoRA and strength. Which design fits?Design
A list of customer LoRAs from customer 1 to customer N (hundreds), one shared SDXL base box, and a box where each request picks its own LoRA and strength.
Schematic of the stem's service.

Further Reading

Code

Ships With