← Master Index
Vol. 17 Module 17.1 Lecture

DDPM

Diffusion Foundations

How This Lesson Fits the Module & Volume

You now have noise, denoising, and latents. DDPM (Ho, Jain, Abbeel, 2020, Denoising Diffusion Probabilistic Models) is the discrete algorithm that made that story trainable: a T-step Markov chain, a variational bound, and a simplified MSE on ε. Every Stable Diffusion checkpoint still speaks this language even when the UI sampler is DDIM or DPM++.

Vol. 16 never showed Algorithm 1. This lecture does. DDIM next keeps the same trained εθ and changes only the reverse chain. FLUX will later replace the discrete VP path with flow matching—still easier if DDPM is in your bones.

Learning Objectives

By the end of this lesson, students should be able to:

  • State the DDPM forward chain q(xt|xt−1) and the closed-form q(xt|x0).
  • Explain that training maximizes a variational lower bound on log pθ(x0), then uses Lsimple.
  • Write Lsimple = Ex0,ε,t [ ||ε − εθ(xt, t)||2 ] and implement the training step.
  • Implement ancestral sampling, including the extra Gaussian at t > 0.
  • Recall the original hyperparameters: T=1000, linear β from 10−4 to 0.02, UNet + self-attention.
  • Know what transfers unchanged when you move DDPM from pixels to SD latents.
Definition

A Denoising Diffusion Probabilistic Model (DDPM) is a latent-variable generative model whose approximate posterior is a fixed Gaussian Markov chain that adds noise for T steps, and whose generator is a learned Markov chain that removes noise. The reverse conditionals are Gaussians whose mean is parameterized by a time-conditioned network εθ (a UNet in the original paper). Training uses the variational bound on −log p(x0), almost always replaced in practice by the unweighted noise-prediction loss Lsimple.

Forward Chain (Fixed)

q(xt | xt−1) = N(√(1−βt) xt−1, βt I), with βt linearly spaced in the original work. Because the chain is Gaussian,

q(xt | x0) = N(√ᾱt x0, (1−ᾱt) I),   xt = √ᾱt x0 + √(1−ᾱt) ε.

No learnable parameters live in q. That is why DDPM training is so stable compared to GANs: the inference path is not a second network.

Reverse Chain (Learned) and the ELBO

We want pθ(x0) = ∫ p(xT) ∏t=1..T pθ(xt−1 | xt) dx1:T with p(xT) = N(0, I). Each reverse step is Gaussian:

pθ(xt−1 | xt) = N(xt−1; μθ(xt, t), Σθ(xt, t)).

Ho et al. fix Σ to a constant schedule (βt I or the posterior β̃t I) and parameterize μθ through εθ:

μθ(xt, t) = (1/√αt) ( xt − (βt / √(1−ᾱt)) εθ(xt, t) ).

The variational bound then decomposes into KL terms between Gaussians (analytic). After dropping constants and reweighting, the working objective is:

Lsimple = Et, x0, ε [ || ε − εθ(√ᾱt x0 + √(1−ᾱt) ε, t) ||2 ].

Algorithm 1 — Training

1. Draw x0

From the dataset (or z0 = scale·E(x)).

2. Draw t, ε

t uniform in 1..T; ε ~ N(0,I).

3. Form xt

Closed-form q_sample.

4. Gradient step

θ ||ε − εθ(xt, t)||2.

import torch import torch.nn.functional as F def ddpm_train_step(model, x0, alphas_cumprod, optimizer): """One minibatch of L_simple. model(x_t, t) -> eps_hat.""" N = x0.size(0) T = alphas_cumprod.numel() t = torch.randint(0, T, (N,), device=x0.device) eps = torch.randn_like(x0) a_bar = alphas_cumprod[t].view(N, *([1] * (x0.ndim - 1))) xt = a_bar.sqrt() * x0 + (1.0 - a_bar).sqrt() * eps eps_hat = model(xt, t) loss = F.mse_loss(eps_hat, eps) optimizer.zero_grad(set_to_none=True) loss.backward() optimizer.step() return float(loss) # In SD-class training, replace x0 with scaled VAE latents and pass text cond into model.

Algorithm 2 — Ancestral Sampling

Start from xT ~ N(0, I). For t = T, …, 1:

xt−1 = (1/√αt) (xt − (βt/√(1−ᾱt)) εθ(xt,t)) + σt z,   z ~ N(0,I) (z=0 if t=1).

Ho et al. use σt = √βt or the true posterior std √β̃t. This extra z is why DDPM samples are stochastic even given εθ and xT. T = 1000 network calls per image is the historical pain point—DDIM attacks it without retraining.

@torch.no_grad() def ddpm_sample(model, shape, alphas, alphas_cumprod, device="cuda"): T = alphas.numel() x = torch.randn(shape, device=device) for t in range(T - 1, -1, -1): eps = model(x, torch.full((shape[0],), t, device=device, dtype=torch.long)) alpha_t = alphas[t] a_bar_t = alphas_cumprod[t] beta_t = 1.0 - alpha_t mean = (1.0 / alpha_t.sqrt()) * (x - (beta_t / (1.0 - a_bar_t).sqrt()) * eps) if t == 0: x = mean else: x = mean + beta_t.sqrt() * torch.randn_like(x) return x

Original Paper vs Production SD

PieceHo et al. 2020SD 1.5-class production
SpacePixels (e.g. 32×32, 256×256)Scaled VAE latent 4×64×64
T / β1000, linear 1e-4→0.02Same discrete math; cosine / scaled-linear variants exist
NetworkUNet, GN, self-attn at 16×16UNet + cross-attention to CLIP
LossLsimple on εSame, plus cond dropout for CFG
Sample steps1000 ancestralOften 20–50 DDIM/DPM++ in the UI

Why DDPM stuck

  • MSE training is boring—in a good way.
  • Mode coverage beats typical GANs.
  • Conditioning slots in cleanly (class, text, mask).

Why UIs abandoned pure ancestral DDPM

  • 1000 steps is too slow for interactive stills.
  • Stochastic z hurts exact reproducibility.
  • Better ODE solvers reuse the same εθ.
Common Misconception

“Lsimple is the ELBO, and DDPM sampling must always use T=1000.” Lsimple is a reweighted, simplified bound—it drops the theoretically correct per-timestep weights and works better in practice. Sampling step count is a discretization choice: you can evaluate a DDPM-trained net with 50 DDIM steps. Conversely, calling a 50-step UI sampler “DDPM” is sloppy; ancestral DDPM implies the stochastic Markov reverse with σt z.

Knowledge Check

  1. Short Answer: Are there learnable parameters in the DDPM forward process q? Answer: No—q is a fixed Gaussian Markov chain.
  2. True/False: Lsimple is MSE between true ε and εθ(xt, t). Answer: True.
  3. Multiple Choice: Original Ho et al. T and linear β range: (a) T=50, β 0.1→0.9, (b) T=1000, β 1e-4→0.02, (c) T=20, cosine only. Answer: (b).
  4. Short Answer: Write μθ in terms of xt, εθ, αt, βt, ᾱt. Answer: μθ = (1/√αt)(xt − (βt/√(1−ᾱt)) εθ).
  5. True/False: During training you must run the reverse chain to compute the loss. Answer: False—training uses one random t and q_sample.
  6. Multiple Choice: The extra z in ancestral sampling makes DDPM: (a) deterministic, (b) stochastic, (c) a GAN. Answer: (b).
  7. Short Answer: What is p(xT)? Answer: Standard normal N(0, I) (isotropic Gaussian prior).
  8. Short Answer: What changes when you train DDPM on SD latents instead of pixels? Answer: x0 becomes scaled z0 = E(x)·scaling_factor; the algorithms stay the same (plus text cond).
  9. Multiple Choice: The ELBO terms between Gaussians are: (a) intractable Monte Carlo only, (b) analytic KLs, (c) adversarial. Answer: (b).
  10. True/False: A checkpoint trained with Lsimple can only be sampled with ancestral DDPM. Answer: False—DDIM and other solvers reuse εθ.

Key Takeaways

  • DDPM = fixed Gaussian forward chain + learned Gaussian reverse + Lsimple on noise.
  • Train with random t and closed-form xt; sample by walking t downward with optional extra noise z.
  • Original recipe: T=1000, linear β, UNet. SD keeps the math and moves it to latents + text.
  • Lsimple is a practical reweighting of the ELBO, not a different generative model.
  • Next: DDIM—same εθ, fewer steps, deterministic option.
Trainer’s Guide

Hands-on idea: Train a 20-minute MNIST DDPM (tiny UNet, T=200). Compare 200-step ancestral samples vs the same net at 20 ancestral steps (quality collapse) vs previewing DDIM at 20 steps next lecture.

Discussion prompt: If Lsimple ignores the theoretically correct ELBO weights, why did Ho et al. still prefer it? (Hint: it up-weights harder mid/high-noise terms that matter for sample quality.)

Recap: DDPM turns diffusion into MSE training and stochastic ancestral sampling. Continue with DDIM.