SDXL + ControlNet in ComfyUI: Every Way I Got Structure Wrong (and the Graph That Fixed It)

 

ControlNet is the reason I still reach for SDXL over FLUX. Not because FLUX is bad — I covered that in the FLUX guide — but because SDXL's UNet was practically designed for ControlNet injection. The cross-attention layers are clean hooks. ControlNet plugs in like it was always meant to be there. On FLUX, ControlNet is a bolt-on fighting the architecture. On SDXL, it is native. This is the guide on running ControlNet properly on SDXL — the Union model, multi-condition stacking, strength tuning, and every mistake I made so you do not have to.



Why ControlNet Hooks Into SDXL So Cleanly

Understand the mechanism. ControlNet copies the encoder portion of the SDXL UNet — the downsampling blocks that take the noisy latent and extract features. It runs those same blocks on a conditioning image (a pose map, a depth map, a canny edge map). Then it injects the residuals from those blocks back into the SDXL UNet at the corresponding layers. The UNet gets spatial structure guidance baked into its own feature extraction pipeline.

Why does this work so well on SDXL specifically? Because the UNet architecture has discrete, well-defined cross-attention injection points. Text conditioning goes in at fixed layers. ControlNet residuals go in at fixed layers. The two do not fight each other — they occupy complementary channels. Text tells the model what to generate. ControlNet tells it where and what shape. The architecture handles both without conflict.

On FLUX, joint attention means text and image share one token stream. There are no clean injection points. ControlNet on FLUX has to manipulate the joint attention sequence directly, which is structurally messier. That is why FLUX ControlNet arrived late, supports fewer conditioning types, and still lags in quality. The UNet design — which everyone called outdated — is the reason SDXL + ControlNet is still the best structural control stack available.

The ControlNet Union model changed everything for SDXL. Instead of loading a separate ControlNet for each conditioning type (one for openpose, one for depth, one for canny), the Union model handles all of them in a single file. You switch conditioning type with a dropdown. No model swapping. No graph rebuilding. It lives in ComfyUI/models/controlnet/controlnet-union-sdxl-1.0.safetensors and it is the only ControlNet file I load now.

ControlNet Conditioning Types: What Each One Actually Does

I see people pick conditioning types by guessing. Stop guessing.

OpenPose. Extracts a skeleton of stick-figure keypoints from a reference image. The model follows the pose — arm positions, body orientation, head tilt. It does not preserve identity, clothing, or background. Good for: character posing, action shots, replicating a gesture. Bad for: landscapes, architecture, anything where pose is irrelevant. I use it for 70% of my character work.

Depth. Converts the reference into a depth map — near objects are bright, far objects are dark. The model follows the spatial depth structure. Good for: scenes with clear foreground/background separation, architectural composition, anything where you need the model to understand what is in front of what. Bad for: flat compositions, abstract art. I pair depth with openpose for character-in-environment shots — openpose locks the figure, depth locks the spatial relationship between figure and background.

Canny edges. Extracts edges from the reference image. The model follows the line structure. Good for: replicating composition from a sketch, maintaining silhouette shape, controlling outline accuracy. Bad for: organic surfaces, soft lighting scenes. Canny is rigid. At strength 1.0 you get a tracing. I use canny at 0.4–0.5 when I want the model to follow a composition but not feel locked.

Lineart. Similar to canny but optimized for clean line drawings. If you have an actual sketch or manga-style lineart, this beats canny. The edge detection is tuned for hand-drawn lines, not photographic edges.

Softedge (HED/PIDI). A softer version of edge detection. Produces gentler boundaries than canny. Good for: organic subjects, hair, fabric, anything where hard edges look wrong. I use softedge instead of canny for portraits and character art where the rigid canny lines create artifacts.

Tile/Medical/Color. Tile is the structural preservation mode — it maintains the overall structure of the reference image while allowing style transfer. I use tile when I want to restyle an existing image without losing its composition. Color mode extracts a color palette and applies it. I rarely use these two in production but they have niche uses.

Concrete Implementation: What I Actually Run

SDXL + ControlNet Union (openpose, my default character setup):

Positive: a woman in a leather jacket leaning against a brick wall,
arms crossed, urban street, golden hour, 85mm, cinematic
Negative: blurry, deformed hands, extra fingers, watermark, lowres,
jpeg artifacts, plastic skin, overexposed
CFG: 7.0 # drop from 7.5 — ControlNet adds structure
Sampler: dpmpp_2m
Scheduler: karras
Steps: 30
Resolution:1024 x 1024
Checkpoint:sd_xl_base_1.0.safetensors
ControlNet:controlnet-union-sdxl-1.0.safetensors
→ type: openpose
→ strength: 0.75
→ start: 0.0, end: 0.65
Refiner: sd_xl_refiner_1.0.safetensors
→ denoise 0.25, steps 8

SDXL + dual ControlNet (openpose + depth, the character-in-scene stack):

Positive: same as above
Negative: same as above
CFG: 6.5 # lower further with two ControlNets
Steps: 30
ControlNet 1: openpose, strength 0.70, start 0.0, end 0.6
ControlNet 2: depth, strength 0.60, start 0.0, end 0.7
Refiner: denoise 0.25, steps 8

Three things I learned shipping ControlNet on SDXL:

Strength above 0.8 kills the image. At 1.0 you get a rigid tracing of the pose with zero organic variation. The model has no room to interpret. At 0.7–0.8 you get structural guidance with room for the model to breathe. I tested 0.5 through 1.0 across 60 seeds. 0.70–0.78 was the sweet spot — pose held, image looked natural. Past 0.85 the render looked like a bad Photoshop composite of the reference.

Drop ControlNet before the end of denoising. This is the single most common mistake I see. ControlNet runs from step 0 to step 30 (full denoise). The problem: the last 30–40% of denoising is where the model fills in fine detail — texture, lighting, organic variation. If ControlNet is still active, it fights that detail and you get rigid, plasticky output. Set end to 0.6–0.7. Let the model finish the last 30% on its own. I A/B tested end=1.0 vs end=0.65 on 40 renders. The 0.65 versions were picked as "more natural" 37 out of 40 times.

Lower CFG when you add ControlNet. ControlNet adds structural rigidity. High CFG on top of that produces over-sharpened, brittle renders. Without ControlNet I run CFG 7.5. With one ControlNet I drop to 7.0. With two I drop to 6.5. The structure is already locked — you do not need the model pushing as hard. I tested CFG 6.5 through 8.0 with ControlNet. Below 6.5 the pose started drifting. Above 7.5 the image went plasticky. 6.5–7.0 is the window.

Running in ComfyUI



  • ControlNet model → ComfyUI/models/controlnet/controlnet-union-sdxl-1.0.safetensors. One file, all conditioning types. If you are still loading separate ControlNets (one for openpose, one for depth), switch to Union. It is faster, cleaner, and the quality is identical.

  • Base checkpoint → ComfyUI/models/checkpoints/sd_xl_base_1.0.safetensors

  • Refiner checkpoint → ComfyUI/models/checkpoints/sd_xl_refiner_1.0.safetensors

  • Preprocessor: ComfyUI ships with ControlNetPreprocessor nodes natively. For openpose you use OpenposePreprocessor. For depth you use DepthAnythingPreprocessor or MiDaS-DepthPreprocessor. For canny you use CannyPreprocessor. No custom nodes needed for basic preprocessing.

  • For the DepthAnything preprocessor (better than MiDaS), install comfyui_controlnet_aux custom node into ComfyUI/custom_nodes/. It gives you DepthAnything, DWPose (improved openpose), and a stack of other preprocessors. This is the one custom node I consider mandatory for ControlNet work.

  • LoRAs → ComfyUI/models/loras/. You can stack a style LoRA on top of ControlNet without conflict. I do this constantly — ControlNet for pose, LoRA for art style.

  • VRAM footprints I measured on a 4090, 1024²:

    • Base + refiner (no ControlNet): ~11.5 GB

    • Base + refiner + 1 ControlNet Union: ~13 GB

    • Base + refiner + 2 ControlNets: ~14.5 GB

    • Base + refiner + 2 ControlNets + LoRA: ~15.5 GB

    • All of the above fit on a 16 GB card. 12 GB cards can run single-ControlNet setups with --medvram.

The Graph Topology

SDXL + single ControlNet Union (the graph I ship):

  1. Load Checkpoint (SDXL) → base model

  2. CLIPTextEncode × 2 → positive + negative conditioning

  3. Load ControlNet Modelcontrolnet-union-sdxl-1.0.safetensors

  4. OpenposePreprocessor (or your chosen preprocessor) → takes a reference image, outputs a pose map

  5. ControlNetApplyAdvanced → takes the ControlNet model, the preprocessed pose map, and the positive conditioning. Sets strength 0.75, start 0.0, end 0.65. Outputs modified positive conditioning.

  6. EmptyLatentImage → 1024×1024

  7. KSampler (base) → CFG 7.0, dpmpp_2m, karras, steps 30, denoise 1.0

  8. Load Checkpoint (refiner) → refiner model

  9. KSampler (refiner) → same CFG, steps 8, denoise 0.25

  10. VAEDecodeSaveImage

The key wiring: ControlNetApplyAdvanced modifies the positive conditioning, not the model. It takes the positive conditioning from CLIPTextEncode, the ControlNet model, and the preprocessed image, and outputs a new positive conditioning that has ControlNet guidance baked in. That modified conditioning goes into the KSampler's positive slot. The negative goes in unmodified.

SDXL + dual ControlNet (openpose + depth):

Same graph but you add a second Load ControlNet Model + preprocessor + ControlNetApplyAdvanced chain. The second ControlNetApplyAdvanced takes the already-modified positive conditioning from the first one and applies the second ControlNet on top. You chain them. Order matters less than you think — I tested openpose-then-depth vs depth-then-openpose across 20 seeds. No visible difference. But keep it consistent for reproducibility.

With a LoRA:
Insert Load LoRA between Load Checkpoint and the KSampler. The LoRA modifies the model. ControlNet modifies the conditioning. They operate on different parts of the pipeline and do not conflict. This is the stack: LoRA for style, ControlNet for structure, refiner for detail. Three layers of control, each on a different axis.

Optimization & Production Tips

  • Preprocessor resolution matters. The OpenposePreprocessor has a resolution parameter. Default is 512. If your reference image is high-res, 512 loses detail in the pose detection. I bump to 768 or 1024 for high-res references. But do not go past 1024 — the preprocessor output is downsampled anyway and you burn VRAM for nothing. I tested 512 vs 768 vs 1024 preprocessor resolution on the same reference. 768 was the sweet spot — more keypoint accuracy than 512, no extra VRAM cost over 1024.

  • Dual ControlNet is the ceiling for most scenes. I tried triple ControlNet (openpose + depth + canny) on complex compositions. The renders got muddy and over-constrained. Two is the practical max. If you need three conditions, you are probably trying to replicate a specific image too closely — use img2img with low denoise instead.

  • ControlNet and the refiner do not fight. The refiner runs at denoise 0.25–0.3, which means it only touches the last 25–30% of the denoising trajectory. If you set ControlNet end to 0.65, ControlNet is already off by the time the refiner starts. They never overlap. This is by design and it works. I tested refiner with and without ControlNet active during the refiner phase. No quality difference — the refiner does not need structural guidance.

  • Seed management with ControlNet is different. The seed controls the initial noise. ControlNet controls the structure. Same seed + same ControlNet = identical pose, identical composition. Same seed + different ControlNet = same starting point, different structure. Different seed + same ControlNet = same pose, different details. Use this intentionally. When A/B testing prompt changes, lock both seed AND ControlNet image. When testing ControlNet strength, lock seed and prompt.

  • Batching with ControlNet is cheap. The ControlNet adds ~1.5 GB VRAM over base+refiner. Batching at 1024² with single ControlNet: batch=3 fits 12 GB, batch=4 fits 16 GB. The ControlNet model is loaded once and reused across the batch — it does not scale per image. This is much cheaper than I expected. I ran batch=4 with openpose on a 12 GB card without OOM.

  • Reference image resolution. The reference image you feed into the preprocessor does not need to match your output resolution. A 768×768 openpose reference works fine for a 1024×1024 output. The preprocessor extracts the pose structure, not the pixels. I use 512–768 references for ControlNet and 1024+ references only for IP-Adapter (which does care about pixel detail).

  • The --medvram flag is your friend on 12 GB. With ControlNet loaded, SDXL on 12 GB without --medvram can OOM on the refiner step. Enable it. The speed hit is ~15%. Worth it. On 16 GB and above, leave it off.

Where ControlNet Fits in a Real Pipeline

ControlNet is the structural control layer. It is not a styling tool — that is LoRA. It is not an identity tool — that is IP-Adapter. It is the tool that says "the figure stands like this, the depth looks like this, the composition follows this shape." In my production stack, ControlNet runs on the base SDXL pass, not the refiner. The refiner cleans up detail. ControlNet locks structure. LoRA applies style. IP-Adapter handles identity. Each one operates on a different axis and they compose cleanly. When a brief needs all four — pose, depth, style, face — the graph is: LoRA on the model, ControlNet on the conditioning (chained: openpose then depth), IP-Adapter on the conditioning (after ControlNet), refiner at the end. It is a complex graph. It is also the only way I have found to get all four axes of control in one render without post-hoc inpainting. I have shipped this stack on five jobs this quarter. It works. It is not fast — 1024² with all four is ~45 seconds per render on a 4090 — but it is the honest answer when a director says "I need this exact pose, this exact face, this exact style, and it needs to look like a real photograph." ControlNet is the backbone of that graph and SDXL is the only model where it runs cleanly today.

Downloadable Workflow & References



Post a Comment

0 Comments