Diffusion Models: Hierarchical VAE with Fixed Encoder
The cleanest way to see diffusion is not as a "score-based generative model" or as "non-equilibrium thermodynamics" — it is as a Variational Autoencoder taken to its logical extreme. Replace the single learned encoder with a chain of T fixed Gaussian encoders. Replace the single learned decoder with one denoising network applied T times in reverse. Train with the same ELBO you derived in the previous lesson, just summed across noise levels instead of evaluated once. Out the other end falls Stable Diffusion 3.5, FLUX, Sora, Imagen, and DALL-E 3.
The generative-AI track has a full lesson on diffusion that walks through the ELBO derivation, classifier-free guidance, and the sampler landscape (DDIM, DPM-Solver++, EDM, LCM). This lesson does something different. It treats diffusion as an architecture story — a particular way of stacking the deep-learning primitives you have already met. The math is in the genai lesson; the architecture is here.
If a colleague asks "is diffusion really just a deep VAE?" the answer they want lives in this lesson. The mechanics of why the U-Net has skip connections, where the time embedding goes, why self-attention shows up at the 16x16 and 8x8 resolutions, what changes when you swap a U-Net for a Diffusion Transformer — those are deep-learning questions. The probability derivations are statistics questions. We are doing the first one.
#The Unifying View: Diffusion is a VAE With T Encoders
In the previous lesson you built a VAE. Encoder maps x to a distribution q(z | x); reparameterize to get a sample z; decoder maps z to a reconstruction; train with the ELBO — reconstruction term plus KL regularizer. One learned encoder, one learned decoder, one stochastic bottleneck.
Now imagine stacking that VAE T times. The first encoder takes x_0 (a clean image) to x_1 (slightly noisy). The second takes x_1 to x_2 (a bit noisier). After T steps you have x_T, which is pure Gaussian noise. The decoder runs in reverse — x_T to x_ to x_ ... back to x_0.
A "hierarchical VAE" with T levels would normally have T learned encoders and T learned decoders. Diffusion makes two ruthless simplifications:
The T encoders are fixed, not learned. Each q(x_t | x_) is a hand-chosen Gaussian noise-addition kernel. No parameters. No training. You just write down a noise schedule and apply it.
The T decoders are one shared network. A single neural network — typically a U-Net or a Diffusion Transformer — is invoked T times with the timestep t as an extra input. The same parameters do all the work at every noise level.
That is the entire reframe. Diffusion is a hierarchical VAE where the encoder is free and the decoder is shared.
This reframe matters because every deep-learning instinct you already have transfers. The decoder is a CNN-ish (or transformer-ish) architecture with the usual building blocks. The training loop is standard supervised learning — predict-a-target with MSE. The hardware footprint is dominated by the same things that dominate CNN training (memory bandwidth on activations, compute on attention). You are not learning a new paradigm; you are learning where to plug the parts in.
Equivalently, sample ε_t ~ N(0, I) and write x_t = √(1 - β_t) · x_ + √(β_t) · ε_t. The factor of √(1 - β_t) keeps the variance from blowing up — at each step we scale the signal down a touch and add a touch of noise. This is the variance-preserving parameterization (Song & Ermon 2021 named it that). After T steps the marginal variance is still bounded above by 1, regardless of the schedule.
The schedule β_t is a hyperparameter, not a learned weight. The original DDPM (Ho 2020) used a linear schedule from β_1 = 10^ to β_T = 0.02 over T = 1000 steps. Modern training uses cosine or sigmoid schedules that keep "signal alive" longer at high noise levels — important for high-resolution images where the linear schedule turns the image to mush too fast. The genai-track lesson lists the trade-offs.
Two convenience quantities show up everywhere: α_t = 1 - β_t (the per-step retention factor) and ᾱ_t = Π_^t α_s (the cumulative retention up to step t). With those, the single-shot reparameterization falls out:
q(x_t | x_0) = N(√ᾱ_t · x_0, (1 - ᾱ_t) · I)
In code: x_t = √ᾱ_t · x_0 + √(1 - ᾱ_t) · ε, where ε ~ N(0, I). This identity is the killer feature of the Gaussian-Markov-chain forward process. You never have to simulate T steps during training. To make a training example at any timestep t, sample one ε, multiply, add. Constant time, no chain to walk.
The forward process has no parameters, so it has no gradients. Pure noise injection.
Scrub the timestep slider to watch a clean image dissolve into noise on the forward pass, then reverse it step by step as the denoiser rebuilds the signal.
Loading visualization...
What Do You Think?
A diffusion model has T=1 — exactly one forward noising step from x_0 to x_1 = √(1-β_1)·x_0 + √β_1·ε, where β_1 is large enough that x_1 looks roughly Gaussian. The reverse process learns to recover x_0 from x_1 in one shot. What does this 1-step diffusion model reduce to?
The reverse process has to go the other way — given x_t, produce x_. We parameterize it as Gaussian too:
p_θ(x_{t-1} | x_t) = N(μ_θ(x_t, t), σ_t² · I)
Two practical simplifications, both due to Ho 2020:
The variance is fixed, not learned. Most implementations set σ_t² = β_t (or the closed-form posterior variance β̃_t = (1-ᾱ_)/(1-ᾱ_t) · β_t). Either works. The network only predicts the mean. Nichol & Dhariwal 2021's improved-DDPM did learn the variance for slightly better log-likelihoods, but it costs an extra head and most production code skips it.
The mean is parameterized by predicted noise, not by a direct mean head. Instead of outputting μ_θ(x_t, t) directly, the network outputs a noise prediction ε_θ(x_t, t) and the mean is reconstructed by the closed-form formula:
This is just algebra — the same formula you would get by inverting the forward identity x_t = √ᾱ_t · x_0 + √(1 - ᾱ_t) · ε and substituting back. The full ELBO-to-noise-prediction derivation lives in the generative-AI track lesson on diffusion (section "From Variational Bound to L_simple") — refer there if you want the six-step proof. For this lesson, treat the noise parameterization as the practical default: it is what every production codebase actually trains.
The crucial architectural fact: one neural network handles all T noise levels. There is no per-timestep decoder. The same parameters get a timestep input t (after sinusoidal embedding) and learn to be a general-purpose denoiser. This parameter sharing is what makes diffusion trainable at all — you cannot afford T = 1000 separate networks, but you can afford one network that knows how to read its current noise level off a clock.
This is the deep-learning meat of the lesson. The shared decoder in every "classical" diffusion model from DDPM (2020) through Stable Diffusion 2.1 (2022) is a U-Net — the same architecture that won medical-image segmentation in 2015 and is now repurposed for "input is noisy x_t, output is predicted noise ε". Every piece of that U-Net is there for a reason.
The defining feature of a U-Net is the symmetric encoder-decoder shape with skip connections from each encoder block to the matching decoder block at the same resolution. The encoder downsamples spatially (64 → 32 → 16 → 8) while growing channels (128 → 256 → 512 → 1024). The decoder mirrors this — upsamples spatially while shrinking channels. At each resolution, the encoder's output is concatenated (or added) to the decoder's input before the next upsampling block.
Why skip connections matter for diffusion specifically:
Low-frequency content lives in the bottleneck, but high-frequency detail lives in the early-layer activations. A denoising decoder needs both. Without skip connections the decoder has to reconstruct fine detail purely from a small bottleneck representation, which is exactly the VAE blurriness problem.
The input x_t and the target ε are spatially aligned at full resolution. A pixel of noise in x_t at coordinate (i, j) is the same pixel of ε to predict at (i, j). The skip connections give the network a direct path from input pixels to output pixels — it does not have to relay every pixel through the bottleneck.
Gradient flow. Skip connections are residual-style: gradients can backprop through the short path during training, avoiding vanishing-gradient pain in a network that is 30-50 layers deep.
If you remove the skip connections, your "U-Net" becomes a plain encoder-decoder, and the samples come out as recognizable shapes drowning in muddy texture — the classic symptom of a model that knows where things are but not what they look like up close.
Quick check
You strip the skip connections out of a diffusion U-Net and retrain from scratch with the same loss, same schedule, same data. The MSE loss converges to roughly the same value as before. What happens to sample quality?
One network has to handle T = 1000 different noise levels. The trick is to feed t as an input. But t is a scalar integer; you cannot just paste it into a convolutional feature map. The standard recipe (borrowed straight from transformer positional encoding):
Sinusoidal time embedding. Map t to a vector of sines and cosines at geometrically-spaced frequencies — the same [sin(t/10000^{2i/d}), cos(t/10000^{2i/d})] formula as Vaswani et al. 2017. Result: a 128- or 256-dimensional dense vector that encodes t smoothly.
MLP projection. Pass that sinusoidal vector through a 2-layer MLP (with SiLU/Swish activation) to produce a "time embedding vector" of the right dimensionality to add into the residual blocks.
Injection into every block. Inside each residual block, the time embedding is projected to match the channel count and added to the feature map after the first conv-norm-activation. Every block at every resolution gets to read the clock.
The sinusoidal-plus-MLP encoding is not the only choice (you can use a learned embedding table indexed by t, or even a continuous formula), but the sinusoidal version is what DDPM, ADM, and Stable Diffusion all use. It gives the network a smooth, dense, frequency-rich representation of t that generalizes to interpolated timesteps — important when you later sample with 50 steps from a model trained on 1000.
Inside each U-Net stage there are 2-3 residual blocks. The modern recipe (since ADM, Dhariwal & Nichol 2021):
GroupNorm instead of BatchNorm. BatchNorm depends on batch statistics, which behave badly when your batch contains samples at very different noise levels (which diffusion always does). GroupNorm normalizes within channel groups of a single sample — stable across noise levels.
SiLU activation (also called Swish) — x · sigmoid(x). Smoother than ReLU around zero, slightly better empirical results in diffusion. Same activation transformers settled on.
Conv 3x3, dropout (optional), conv 3x3, residual add. Two 3x3 convolutions per block with a residual connection — the same shape Kaiming He's ResNet introduced in 2015.
Time embedding addition between the two convs, after the first norm + activation. The block reads the clock once and propagates it.
The point of all of this is that a diffusion U-Net is just a stack of well-tuned ResNet blocks with a clock signal threaded through. There is no new mathematical apparatus. Everything you learned in the CNN and normalization lessons applies.
Pure convolution has a finite receptive field. At early layers and full resolution, that is fine — local denoising is a local operation. But once you have downsampled to 16x16 or 8x8, you want the network to reason globally about the image: "the left side is a face, so the right side should also look face-y", "this is a horse, so its legs should match", "this brushstroke is impressionist, propagate that style everywhere".
The fix: insert multi-head self-attention layers at the low-resolution stages of the U-Net. DDPM added attention at 16x16 only. ADM (Dhariwal & Nichol 2021) extended it to 16x16 and 8x8. Stable Diffusion does attention at all spatial resolutions in its operating latent space. The attention layer treats spatial positions as a sequence and lets every position attend to every other.
Why only at low resolution? Self-attention is O(n²) in sequence length. At 64x64 that is 64² = 4096 tokens, with a 4096² = 16M-element attention matrix — too expensive at every layer. At 8x8 it is 64 tokens, 4096-element matrix — cheap. So you keep the local conv operations at high resolution and pay for global reasoning only at the bottleneck.
Pure DDPM is unconditional — feed in pure noise, get out a random sample from p(x). To make a text-to-image model, you need a way to inject conditioning information (a text embedding, a class label, a low-resolution sketch) into the denoising network.
The standard trick is cross-attention. The conditioning information c (typically a sequence of text tokens encoded by a frozen CLIP or T5) is treated as a key-value sequence. The spatial features of the U-Net are treated as a query sequence. Cross-attention lets each spatial location pull information from the relevant parts of the text embedding:
Q = W_Q · spatial_features
K = W_K · text_embeddings
V = W_V · text_embeddings
attention_output = softmax(Q K^T / √d) · V
This is the mechanism that lets Stable Diffusion know that "a cat on a skateboard" means certain spatial regions of the latent should be cat-pixels and others should be skateboard-pixels. The cross-attention layers are interleaved with the self-attention layers in every block at every conditioning-eligible resolution.
By 2023, the field had started replacing U-Nets entirely with Diffusion Transformers. The trigger was William Peebles and Saining Xie's ICCV 2023 paper "Scalable Diffusion Models with Transformers" (DiT), which showed that a vision-transformer backbone trained as a diffusion decoder scales better than a U-Net at high parameter counts. The architecture is plain ViT — patchify the input, stack transformer blocks — with the time and conditioning information injected via adaptive layer norm (AdaLN-Zero).
Modern production models that use DiT or DiT-like architectures:
Stable Diffusion 3 / 3.5 (Stability AI 2024) — uses MM-DiT (Multimodal Diffusion Transformer), where image tokens and text tokens flow through joint transformer blocks instead of being injected via cross-attention. Big quality jump over the SD2 U-Net.
FLUX.1 (Black Forest Labs 2024) — also MM-DiT, currently the strongest open-weights image model. The team is largely the original Stable Diffusion authors.
Sora (OpenAI 2024) — DiT applied to video. Patches are now spatiotemporal "cubelets" instead of 2D patches; otherwise it is the same recipe.
Imagen 3 (Google DeepMind 2024) — uses a transformer backbone with text-conditioning baked in at every layer.
The migration matters because it tells you what the deep-learning track owes the transformer track. A modern diffusion model is much closer to "a ViT trained with a noise-prediction objective" than to "a U-Net". The skip connections become learned residual connections inside transformer blocks; the cross-attention is replaced by joint self-attention over [image_tokens ; text_tokens]; the time conditioning is replaced by AdaLN-Zero. But the conceptual story — fixed encoder chain, shared decoder, noise-prediction loss — is unchanged.
Quick check
A team is migrating their text-to-image pipeline from a U-Net (Stable Diffusion 2.1) to an MM-DiT (Stable Diffusion 3.5). Same training data, same compute budget. Which of the following is the BEST description of what changes architecturally?
Diffusion training is shockingly straightforward — it looks more like supervised classification training than the GAN dance of the previous lesson. The full algorithm:
for each minibatch x_0 from the dataset:
t ~ Uniform({1, ..., T}) # one random timestep per sample
ε ~ N(0, I) # one Gaussian noise per sample
x_t = √ᾱ_t · x_0 + √(1 - ᾱ_t) · ε # forward reparameterization
ε̂ = ε_θ(x_t, t) # forward pass through the U-Net
L = ||ε - ε̂||² # MSE on noise prediction
backprop, Adam step
That is it. No discriminator, no balance to maintain, no special tricks. Sample a random timestep per example, jump straight to x_t in one step, predict the noise, MSE, step. Training is embarrassingly parallel — every (t, x_0) pair is independent, no chain to walk, no autoregressive dependency. A single A100 trains a competitive 64x64 diffusion model on CIFAR-10 in under a day.
The L_simple loss above is the practical training objective from Ho 2020. It comes from a particular re-weighting of the ELBO that the genai-track lesson derives in full. For this lesson the takeaway is: the architecture you just learned, trained with one-line MSE on noise prediction, IS a diffusion model. Everything else is sampling-time machinery and conditioning hooks.
Practical training notes worth knowing:
EMA on weights. Almost all production diffusion models track an exponential moving average of weights with decay 0.999 or 0.9999 and use the EMA weights for sampling. The training weights are noisier; the EMA is what produces good samples. Standard trick from supervised CV, doubly important here.
Mixed precision. Train in bfloat16 or float16 with a small float32 master copy of weights. Saves 2x memory and runs 2-3x faster on modern GPUs. The MSE loss is well-conditioned enough that this works without numerical issues.
Gradient checkpointing on the U-Net activations. Lets you fit larger batches at the cost of an extra forward pass.
Conditioning dropout (10%). During training, with 10% probability, replace the text conditioning with a null token (an "empty prompt" embedding). This makes the same model usable for both conditional and unconditional generation — the prerequisite for classifier-free guidance at sampling time.
#Hands-On 1: Forward Diffusion and a Toy Denoiser on 2D Data
Time to see this run. The 2D toy case is the cleanest demo — you can plot the data distribution at every timestep and watch it morph into N(0, I).
Loading visualization...
A few things are worth noticing in this code.
The forward process has zero parameters. Nothing about q_sample is learned. It is pure noise injection on a fixed schedule. Look at the snapshot plots — the clusters smear out into an isotropic Gaussian, deterministically (in distribution) from the schedule.
One network handles all timesteps. The same (W1, W2, W3) weights produce the noise prediction at t=5 as at t=95. The time embedding tells it which noise level it is looking at.
Training is a single MSE. No discriminator, no balance, no warmup tricks. Sample (x_0, t, ε), compute x_t, MSE on predicted noise, step.
Sampling is iterative. The DDPM sampler walks T=100 reverse steps. The model is called 100 times for each generated sample. This is the cost diffusion pays for stable training and stable sampling: many forward passes per sample.
This is a toy. A production diffusion model replaces the 2-layer MLP with a U-Net (or DiT), the 2D Gaussian mixture with a million-image dataset, T=100 with T=1000 (or fewer with modern flow-matching schedules), and the linear schedule with cosine. But the loop is character-for-character identical.
The original DDPM sampler walks the full Markov chain in reverse, one step at a time, with a small noise injection at every step:
x_T ~ N(0, I)
for t from T down to 1:
z ~ N(0, I) if t > 1 else 0
x_{t-1} = (1/√α_t) · (x_t - (β_t / √(1-ᾱ_t)) · ε_θ(x_t, t)) + σ_t · z
return x_0
This is faithful to the variational derivation and the theory is clean. It requires roughly T forward passes through the U-Net per sample — for T=1000 that is 1000 network evaluations to make a single image. At 50ms per pass on a fast GPU that is 50 seconds per sample. Hopeless for interactive use.
Song, Meng, and Ermon's 2021 DDIM paper showed that the reverse process can be reformulated as a deterministic, non-Markovian update that lets you skip timesteps. The reverse step becomes:
Two things change. First, no + σ_t · z term — the sampler is fully deterministic given the initial x_T. Second, the relationship between consecutive ᾱ_t values is the only thing that matters; you can skip steps and the formula still holds with the matching ᾱ lookup. Pick 50 evenly-spaced timesteps out of the original 1000 and you get a sample that is visually indistinguishable from the full 1000-step DDPM output, at 20x the speed.
The deterministic nature has a side effect that turned out to be very useful: a fixed initial noise x_T maps to a fixed output, so you get reproducible generation. Same seed, same prompt, same image. Every "txt2img with a seed" feature in every diffusion UI relies on this.
Loading visualization...
The picture you should walk away with from this playground:
DDPM and DDIM at 1000 steps look the same. They produce indistinguishable samples — DDIM is deterministic, DDPM is stochastic, but the marginal distributions match.
DDIM at 50 steps loses almost nothing visible. This is the 20x speedup that made diffusion practical for production. Stable Diffusion's default is 20-50 DDIM-class steps.
DDIM at 10 steps is where the trade-off bites. The clusters start to bleed into each other; the model does not get enough denoising passes to fully resolve the structure.
The trained model is the same in every case. The sampler is a runtime choice. You train once with L_simple at T=1000; you sample with whichever scheduler your latency budget allows.
Production samplers (DPM-Solver++, EDM-Heun, etc.) push the trade-off curve further — they hit DDIM-1000 quality at 15-25 steps by solving the underlying probability-flow ODE with higher-order numerics. The genai-track lesson has the full sampler cheat sheet.
A team trains a diffusion model with the cosine noise schedule (the modern default that keeps signal alive longer at high t). At inference time, by accident, they load the linear noise schedule from a default config file. They run DDIM-50 sampling. What is the most likely outcome?
The headline difference is the inference cost. Diffusion pays for stable training and sharp samples by running the decoder T times instead of once. That cost is the only structural weakness, and it is what every "distillation" line of research (LCM, SDXL Turbo, Consistency Models, Stable Diffusion Turbo) is trying to close.
A practical thing worth knowing: diffusion training is comparable in compute cost to large autoregressive language models. SDXL was trained on roughly 5x10^22 FLOPs (around 256 A100s for a month). FLUX.1 [pro] is on the same order. Sora is significantly larger — OpenAI has not published exact numbers, but estimates put it in the same range as GPT-4 training. The cost is dominated by the U-Net (or DiT) forward and backward passes, multiplied by the number of training steps, multiplied by the batch size, multiplied by the spatial size of x_t.
Inference is the more visible cost. Naive DDPM at T=1000 with classifier-free guidance is 2000 U-Net forward passes per image. A 1024x1024 SDXL pass is roughly 10 GFLOPs of compute on the U-Net plus 50 MFLOPs on the VAE decoder. At 2000 passes that is 20 TFLOPs — about 3 seconds on a single H100. With DPM-Solver++ at 25 steps and bf16 you are down to 1.4 seconds. With SDXL Turbo at 1 step you are at 150ms — fast enough for interactive UIs. The compute envelope is the engineering constraint that determines whether a diffusion model can be exposed as a "real-time" feature versus a "wait two seconds" feature.
Distillation is the lever. Latent Consistency Models (Luo et al. 2023) train a student diffusion model to take 4-8 steps total, distilled from a teacher running the full 50-step chain. Consistency Models (Song et al. 2023) train a network to predict the endpoint x_0 directly from any x_t in one step — abandoning the iterative reverse process entirely. Stable Diffusion Turbo and FLUX Schnell are production distilled models. The frontier is pushing toward 1-2 step sampling at full quality.
Diffusion is a hierarchical VAE with T fixed Gaussian encoders and one shared learned decoder. The forward process injects noise on a fixed schedule; the reverse process predicts that noise with a single neural network applied T times. Everything else is engineering.
The denoising U-Net is a stack of ResNet blocks with a time embedding threaded through and self-attention at low resolutions. Skip connections carry high-frequency detail past the bottleneck. Cross-attention injects text conditioning. Modern flagship models replace the U-Net with a Diffusion Transformer (DiT) or Multimodal DiT.
Training is an MSE on predicted noise. No discriminator, no adversarial balance, no posterior collapse. Sample (x_0, t, ε), compute x_t, predict ε, backprop. Embarrassingly parallel and pleasantly stable.
Sampling cost is the structural weakness. DDPM needs T forward passes per sample; DDIM and modern samplers cut this to 20-50; LCM and Turbo distill down to 1-4 steps with mild quality loss.
Classifier-free guidance is the single most important inference-time trick. Train one model that handles both conditional and unconditional inputs, then linearly extrapolate the noise prediction toward the conditional direction at sampling time.
Generative-AI track lesson on diffusion. Full ELBO-to-L_simple derivation, sampler taxonomy, classifier-free guidance with code, and the latent-diffusion / Stable Diffusion family
Stochastic Calculus & SDEs in Math Foundations — the continuous-time view of diffusion: Song et al. 2021's score-SDE formulation, the Karras EDM framework, why DDPM is the Variance-Preserving SDE and score matching is the Variance-Exploding SDE, and how Brownian motion and the Fokker-Planck equation underpin every modern sampler
Track-06 lesson on flow matching and rectified flow. The post-diffusion paradigm that FLUX and SD3 actually use for training, with the same MM-DiT architecture but a different training objective
Track-06 lesson on controllable generation. ControlNet, IP-Adapter, LoRA for steering the diffusion architecture you learned here
You now have the architecture story straight. For the probability derivations — why L_simple drops the variational weights, why classifier-free guidance is mathematically a score reweighting, why DDIM works for any subset of timesteps — head over to the generative-AI track lesson on diffusion. The math there is the explanation; the architecture here is the why.
Mode coverage
Captures full distribution
Prone to mode collapse
Captures full distribution
Compute at training
One forward/backward per step
Two networks alternating
One forward/backward per step
Compute at sampling
1 forward pass
1 forward pass
T forward passes (with mitigation by DDIM/distillation)
Density estimation
Lower bound (ELBO)
None (implicit)
Score function / log-likelihood via ELBO
Latent interpretability
Smooth, semantically meaningful
Brittle, depends on architecture
x_T is pure Gaussian; intermediate x_t encodes coarse-to-fine structure
The current state in 2026: diffusion has fully displaced GANs as the dominant generative paradigm for images and video, and the architecture has converged on MM-DiT for new flagship models. The pace of improvement comes mostly from scale, better data, and better samplers — the core training objective is still essentially what Ho 2020 wrote down.