Modalities

Image generation: diffusion and kernels

Diffusion models turn inference on its head. Instead of one forward pass, you run 20 to 50 iterative denoising steps, each involving a full UNet or transformer forward pass. The optimization surface is rich: scheduler tuning, attention kernels, VAE decoding, and classifier-free guidance all compete for your GPU budget.

Coming from LLM inference, diffusion models felt alien. In LLM serving, the core loop is autoregressive: generate one token, feed it back, repeat. In diffusion, the core loop is iterative denoising: start with random noise, predict and remove a bit of noise, repeat 20 to 50 times. Each step is a full forward pass through a large neural network. The entire image is refined simultaneously, not pixel by pixel.

This means the optimization landscape is completely different from what I had been working with, and it took me a while to build the right mental model.

How diffusion inference works

The standard Stable Diffusion pipeline (and its successors like SDXL and SD3) has three components:

# Simplified diffusion inference loop
import torch
from diffusers import StableDiffusionPipeline

pipe = StableDiffusionPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16,
).to("cuda")

# What happens inside pipe():
# 1. Text encoding (once):
#    text_embeds = text_encoder(prompt_tokens)
#
# 2. Start with random noise:
#    latents = torch.randn(1, 4, 128, 128)  # latent space
#
# 3. Iterative denoising (N steps):
#    for t in scheduler.timesteps:   # e.g., 30 steps
#        noise_pred = unet(latents, t, text_embeds)
#        latents = scheduler.step(noise_pred, t, latents)
#
# 4. VAE decode (once):
#    image = vae.decode(latents)

image = pipe("a cat wearing a space helmet", num_inference_steps=30).images[0]

The compute profile

Unlike LLM decode (memory-bound) or embedding inference (compute-bound on small models), diffusion inference is solidly compute-bound. The UNet in SDXL has about 2.6 billion parameters, and you run it 30 times. That is roughly 78 billion parameter-forward-passes per image. The DiT in SD3 Medium is 2 billion parameters, and Flux Dev is 12 billion.

On an A100, a single SDXL image at 1024x1024 with 30 steps takes about 5 to 8 seconds in FP16. The breakdown is roughly:

This tells you exactly where to focus optimization: the denoising loop.

Reducing the number of steps

The highest-impact optimization for diffusion is not a kernel trick. It is reducing the number of denoising steps. The scheduler (also called the sampler or solver) controls how many steps are needed to produce a good image:

Going from 30 steps to 8 steps gives roughly a 3.75x speedup with no other changes. The quality tradeoff depends on the specific model and scheduler combination, but for many production use cases (thumbnails, previews, iterative design), 8-step generation is perfectly acceptable.

Step count is your biggest lever

Before spending a week on kernel optimization, try a better scheduler. DPM++ 2M Karras at 20 steps often matches DDIM at 50 steps in quality. LCM-LoRA at 4 steps is fast enough for real-time preview workflows. The scheduler choice alone can be worth more than switching GPU generations.

Classifier-free guidance and its cost

Most text-to-image models use classifier-free guidance (CFG): at each denoising step, the model runs twice, once with the text conditioning and once without (the "unconditional" pass). The two predictions are blended to steer the output toward the prompt:

# CFG doubles the compute per step
noise_pred_uncond = unet(latents, t, null_text_embeds)  # unconditional
noise_pred_text = unet(latents, t, text_embeds)          # conditioned

# Blend: steer toward the text prediction
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)

This means every denoising step costs 2x the compute. For 30 steps with CFG, you are running the UNet 60 times. Optimizations that eliminate or reduce CFG are therefore very valuable:

Attention kernel optimization

The UNet and DiT architectures use self-attention and cross-attention layers, just like LLMs. The same attention kernel optimizations apply:

# Enable memory-efficient attention in diffusers
pipe = StableDiffusionXLPipeline.from_pretrained(model_id, torch_dtype=torch.float16)
pipe.to("cuda")

# Option 1: xformers (if installed)
pipe.enable_xformers_memory_efficient_attention()

# Option 2: PyTorch SDPA (default in recent diffusers)
# Automatically used when xformers is not explicitly enabled
# Uses FlashAttention-2 when available

Compilation and graph optimization

Because the denoising backbone runs the same computation 20 to 50 times with different inputs, it is an ideal candidate for graph compilation:

In practice, torch.compile is the easiest win for diffusion inference. The compilation cache means you pay the compile cost once and reuse it across all subsequent images.

VAE decoding: the forgotten bottleneck

The VAE decoder converts the small latent representation (128x128 for SDXL at 1024x1024 output) into full-resolution pixels. It is a single forward pass, but for high-resolution outputs it can take 200 to 500 ms. Two optimizations help:

Batching and throughput

Diffusion batching is straightforward compared to LLMs. All images in a batch go through the same number of denoising steps (assuming the same scheduler settings), so there is no variable-length padding issue. You simply stack the latents and text embeddings along the batch dimension.

The constraint is memory. A single SDXL generation at 1024x1024 in FP16 uses about 8 to 10 GB of GPU memory. An A100 with 80 GB can batch roughly 6 to 8 images simultaneously. For throughput-optimized serving, you want to fill the GPU with a batch and run all steps together.

Diffusion inference inverts the LLM optimization playbook. In LLMs, you fight memory bandwidth during decode. In diffusion, you fight compute during denoising. In LLMs, more steps means more tokens (good). In diffusion, fewer steps means faster images (also good). The only constant is this: measure first, then optimize the actual bottleneck.

This wraps up the core modalities: embeddings, ASR, TTS, and now image generation. Next we will look at video generation, where context parallelism becomes essential because the spatial and temporal dimensions of video push even the largest GPUs to their limits.