TAI BUI
Back to curriculum
BuildPython

Image Generation — Diffusion Models

A diffusion model learns one trick: strip a little noise out of a noisy image. Run that trick backwards a thousand times starting from pure static, and an image appears.

TIME: ~75 minutesPREREQUISITES: Phase 4 Lesson 07 (U-Net), Phase 1 Lesson 06 (Probability), Phase 3 Lesson 06 (Optimizers)

Learning Objectives

  • Derive the forward noising process `x_0 -> x_1 -> ... -> x_T` and explain why the closed-form `q(x_t | x_0)` holds for any t
  • Implement a DDPM-style training objective that regresses the noise added at each step, and a sampler that walks back from pure noise to an image
  • Build a time-conditioned U-Net (small enough to train on CPU) that predicts the noise for any timestep
  • Explain the difference between DDPM and DDIM sampling, and when each is appropriate (Lesson 23 covers flow matching and rectified flow in depth)

The Problem

GANs generate in one shot: noise in, image out, a single forward pass. That makes them fast and famously hard to train. Diffusion models take the opposite trade: start from pure noise, denoise in small steps, and let the image emerge gradually. Slower, but far easier to train, and for the last five years that second property has won. Any small team can train a diffusion model and get reasonable samples — GAN training is still a craft you learn over years of failed runs.

The bigger payoff is what diffusion's step-by-step structure unlocks: text conditioning, inpainting, image editing, super-resolution, controllable style. Every step in the sampling loop is a place to inject a new constraint, and that single hook is why Stable Diffusion, Imagen, DALL-E 3, Midjourney, and nearly every controllable image model you'll touch are built on diffusion.

This lesson builds the minimal version: forward noising, backward denoising, one training loop. The next lesson (Stable Diffusion) wires that core into a production system with a VAE, a text encoder, and classifier-free guidance.

Warm-up0/3
1. If you took a clear photo and mixed in a small amount of random static, then repeated that hundreds of times, what would you expect the final image to look like?Warm-up
Four small panels: a clear starting picture, then rounds 1, 2 and 3 with a little more random speckle each time, then an unmarked panel with a question mark for after hundreds more rounds.
The setup only: a photo with a little random static mixed in, repeated. The speckle drawn is illustrative.
2. What does it mean to add Gaussian noise to an image?Warm-up
A 3 by 4 grid of pixel values on the left, an arrow labelled add Gaussian noise, and a dashed panel on the right reading after: each pixel equals question mark.
Illustrative pixel values. Only the input side is drawn; the result is left blank.
3. A regression model produces predictions and you have the true target values. What does mean squared error (MSE) compute?Warm-up
A table of five predictions and their five target values, with a dashed box beside it reading MSE equals question mark.
Illustrative prediction and target pairs, not numbers from the lesson.
Check: The Problem0/2
1. Compared with a GAN, which trade-off does a diffusion model make?Explain
A two row by two column grid, rows GAN and diffusion, columns sampling and training, with all four cells drawn as dashed question-mark boxes.
The two columns The Problem section weighs, with every cell left blank.
2. You are asked to add a constraint such as inpainting or text guidance to a diffusion model. Which property of the sampling process makes this feasible?Design
A row of seven circles linked by arrows between labels x_T and x_0, noted as one sampler step each, with a dashed box reading why a constraint can be added equals question mark.
The lesson's sampling loop as given; the reason a constraint can attach is left blank.

The Concept

The forward process

Take an image x_0. Add a tiny amount of Gaussian noise to get x_1. Add a tiny amount more to get x_2. Keep going for T steps until x_T is nearly indistinguishable from pure Gaussian noise.

q(x_t | x_{t-1}) = N(x_t; sqrt(1 - beta_t) * x_{t-1},  beta_t * I)

beta_t is a small variance schedule, typically linear from 0.0001 to 0.02 over T=1000 steps. Each step slightly shrinks the signal and injects fresh noise.

The closed-form jump

Adding noise one step at a time is a Markov chain (each step depends only on the state right before it, not the full history), but the math folds: you can sample x_t directly from x_0 in one step.

Define alpha_t = 1 - beta_t
Define alpha_bar_t = prod_{s=1..t} alpha_s

Then:
  q(x_t | x_0) = N(x_t; sqrt(alpha_bar_t) * x_0,  (1 - alpha_bar_t) * I)

Equivalently:
  x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon
  where epsilon ~ N(0, I)

This single equation is the whole reason diffusion is practical: during training you pick a random t, sample x_t directly from x_0, and train in one step. No simulation of the full Markov chain required.

The reverse process

The forward process is fixed. The reverse process p(x_{t-1} | x_t) is what the neural network learns. Diffusion models do not predict x_{t-1} directly; they predict the noise epsilon added at step t, and the math derives x_{t-1} from it.

Mermaid diagram

The training loss

For every training step:

  1. Sample a real image x_0.
  2. Sample a timestep t uniformly from [1, T].
  3. Sample noise epsilon ~ N(0, I).
  4. Compute x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon.
  5. Predict epsilon_theta(x_t, t) with the network.
  6. Minimise || epsilon - epsilon_theta(x_t, t) ||^2.

That's it. The network learns to predict the noise at any timestep. The loss is plain MSE: no adversarial game, no collapse, no oscillation.

The sampler (DDPM)

To generate: start from x_T ~ N(0, I) and walk backwards one step at a time.

for t = T, T-1, ..., 1:
    eps = model(x_t, t)
    x_{t-1} = (1 / sqrt(alpha_t)) * (x_t - (beta_t / sqrt(1 - alpha_bar_t)) * eps) + sqrt(beta_t) * z
    where z ~ N(0, I) if t > 1, else 0
return x_0

In general, the reverse conditional has no closed form. For this specific Gaussian forward process, it does, and the ugly-looking coefficients above are exactly what Bayes' rule hands you.

Why 1000 steps

The forward noise schedule is chosen so each step adds just enough noise that the reverse step stays close to Gaussian. Too few steps and the reverse step drifts too far from Gaussian for the network to model well. Too many steps and sampling gets expensive for diminishing gain. T=1000 with a linear schedule is the DDPM default.

DDIM: 20x faster sampling

Training stays the same. Sampling changes. DDIM (Song et al., 2020) defines a deterministic reverse process that skips timesteps without retraining. Fifty steps with DDIM gets near-1000-step DDPM quality. Every production system uses DDIM or an even faster variant: DPM-Solver, Euler ancestral.

Time conditioning

The network epsilon_theta(x_t, t) needs to know which timestep it's denoising. Modern diffusion models inject t via sinusoidal time embeddings: sine and cosine waves at a range of frequencies turn the scalar t into a vector, and that vector gets added to feature maps at every U-Net level. (You'll meet this exact trick again later in the curriculum as positional encoding in transformers.)

t_embedding = sinusoidal(t)
feature_map += MLP(t_embedding)

Skip time conditioning and the network has to guess the noise level from the image itself. That works, but it's far less sample-efficient.

Check: The Concept0/20
1. What is the forward process in a diffusion model?Define
Five panels labelled x_0, x_1, x_2, dots and x_T, the first a clear picture and each later one more speckled, linked by plain arrows.
One concrete run of the chain, with the lesson's own x_0 through x_T labels.
2. What is the reverse process p(x_{t-1} | x_t) in a diffusion model?Define
Five panels running the other way, from x_T, almost pure static, back to x_0, a clear picture, with a small blank marker over each backward arrow.
The direction of travel only (noisy to clean); the mechanism at each step is left blank.
3. What does the network epsilon_theta(x_t, t) output during training?Define
A noisy panel x_t and a scalar t both feeding into a box labelled epsilon_theta of x_t and t, with an arrow out to a blank box marked output equals question mark.
The network's two inputs only; what it outputs is left blank.
4. The lesson defines the forward process as q(x_t | x_{t-1}) = N(x_t; sqrt(1 - beta_t) * x_{t-1}, beta_t * I). What does beta_t represent?Define
A line chart of beta_t over t from 0 to 999, rising almost straight from the bottom of the axis to 0.02, with gridlines at 0, 0.01 and 0.02.
beta_t over t, computed from the lesson's own linear_beta_schedule (T=1000, 0.0001 to 0.02).
5. What is alpha_bar_t, and how does it change as t grows?Define
A small table of beta_t at t equal to 0, 250, 500, 750 and 999 from the lesson's schedule, next to a dashed box reading alpha_bar_t equals question mark.
The beta values alpha_bar_t is built from, computed from the lesson's default schedule.
6. What defines the DDPM sampler?Define
The lesson's DDPM sample code with six lines, two of them highlighted and replaced by a question mark: the timesteps the loop runs over, and the line that updates x.
The lesson's own sample() code with its two defining lines blanked.
7. What defines the DDIM sampler?Define
The lesson's DDIM sample_ddim code with six lines, two of them highlighted and replaced by a question mark: the timestep pairs the loop walks, and the line that updates x.
The lesson's own sample_ddim() code with its two defining lines blanked.
8. What is time conditioning in a diffusion U-Net?Define
Two separate boxes with no connection between them: t, one integer from 0 to 999, and a feature map of 64 channels from the lesson's TinyUNet, with a dashed box below reading time conditioning equals question mark.
The two concrete objects from the lesson's own TinyUNet, with nothing drawn between them.
9. Why is the closed-form expression for q(x_t | x_0) what makes diffusion training practical?Explain
Four panels x_0 through x_t linked by a step-by-step chain of arrows along the top, and a single dashed curved path running well below the labels straight from x_0 to x_t.
Two routes from x_0 to x_t exist on paper, the step-by-step chain and a direct jump; neither is marked as the one training uses.
10. In x_t = sqrt(alpha_bar_t) * x_0 + sqrt(1 - alpha_bar_t) * epsilon, what happens to the balance of signal and noise as t grows toward T?Explain
A card giving the two weights at t equal to 200, 0.8102 for x_0 and 0.5862 for noise, above a dashed box asking for the same two weights at t equal to 999.
One timestep's weights, computed from the lesson's default schedule; the other is left blank.
11. Why can diffusion training use a plain MSE loss instead of an adversarial game?Explain
A flow with x_0, t and a third dashed box marked question mark feeding a build x_t box, then a model box producing eps_theta, and below it a loss card reading F.mse_loss of eps_theta and a blank.
The lesson's training step with the third input and the loss target both left blank.
12. In the DDPM sampler, each backward step computes x_{t-1} = (1/sqrt(alpha_t)) * (x_t - (beta_t/sqrt(1 - alpha_bar_t)) * eps) + sqrt(beta_t) * z. What is the role of the model's output eps?Explain
A box labelled model of x_t and t with an arrow to eps, then an arrow to a dashed box reading used for question mark, above a code card repeating the stem's own three-line formula.
The stem's formula redrawn, with eps traced back to the model; what eps feeds is left blank.
13. The lesson explains why diffusion models typically use around T=1000 steps. What tradeoff does this number balance?Explain
A card with the first and last step of the lesson's schedule, each showing its beta and the fraction of signal kept, above a dashed box reading what the choice of T balances equals question mark.
Per-step numbers computed from the lesson's default schedule; what T trades off is left blank.
14. Why can DDIM reach similar sample quality to DDPM with roughly 20x fewer steps?Explain
Five linked boxes for data and resolution, network architecture, training objective, trained weights and sampling loop, with a dashed box below reading DDIM changes question mark.
The stages a change could land on, none of them marked.
15. How does a diffusion U-Net receive the timestep t?Explain
The lesson's five-block U-Net chain enc1, enc2, mid, dec1, dec2 under a dashed bracket spanning all five, a separate box for t above, and a dashed question-mark box below standing for the path between them.
The U-Net's block chain and the scalar t, with no path drawn between them.
16. Suppose you strip timestep conditioning entirely: the U-Net receives x_t but never learns which t it is looking at. What does the lesson say is the most likely consequence?Explain
A box for the scalar t joined to the five-block U-Net chain by a line with a red cross drawn across it, labelled cut, and a dashed box below reading effect on training equals question mark.
The scenario only: t's path into the network is cut; the consequence is left blank.
17. For a T=1000 run, you find alpha_bar_t is already about 0 at t=600. What does that imply about x_t for t from 600 to 1000?Analyze
A line chart of alpha_bar_t falling from near 1 toward 0 well before t=600, with a dashed marker at t=600 labelled given: about 0 here.
A schedule computed so alpha_bar_t is already about 0 by t=600, as the question states; what that implies for x_t is left for the question.
18. A teammate says switching from the 1000-step DDPM sampler to 50-step DDIM requires retraining with a different loss. What is the right response?Analyze
A quote card with a teammate's claim that switching samplers needs retraining with a different loss, and a blank box asking whether that claim is true or false.
The claim as given, with no verdict drawn yet.
19. To build x_t during training, a teammate loops the one-step update q(x_t | x_{t-1}) t times starting from x_0. What is the problem with this design?Analyze
A card describing a plan to reach x_t by applying the one-step forward update in a loop, t times, starting from x_0, with a blank box asking what is wrong with the plan.
The teammate's plan as given: loop the one-step update t times; what is wrong with it is left blank.
20. A teammate argues that p(x_{t-1} | x_t) always has a closed form for any forward process, so no learned network is needed. What is wrong with this claim?Analyze
A quote card with a teammate's claim that the reverse step always has a closed form for any forward process, and a blank box asking whether the claim is true or false.
The claim as given, with no verdict drawn yet.

Build It

Step 1: Noise schedule

python
import torch
 
def linear_beta_schedule(T=1000, beta_start=1e-4, beta_end=2e-2):
    return torch.linspace(beta_start, beta_end, T)
 
 
def precompute_schedule(betas):
    alphas = 1.0 - betas
    alphas_cumprod = torch.cumprod(alphas, dim=0)
    return {
        "betas": betas,
        "alphas": alphas,
        "alphas_cumprod": alphas_cumprod,
        "sqrt_alphas_cumprod": torch.sqrt(alphas_cumprod),
        "sqrt_one_minus_alphas_cumprod": torch.sqrt(1.0 - alphas_cumprod),
        "sqrt_recip_alphas": torch.sqrt(1.0 / alphas),
    }
 
schedule = precompute_schedule(linear_beta_schedule(T=1000))

Precompute once, gather by index during training and sampling.

Step 2: Forward diffusion (q_sample)

python
def q_sample(x0, t, noise, schedule):
    sqrt_a = schedule["sqrt_alphas_cumprod"][t].view(-1, 1, 1, 1)
    sqrt_one_minus_a = schedule["sqrt_one_minus_alphas_cumprod"][t].view(-1, 1, 1, 1)
    return sqrt_a * x0 + sqrt_one_minus_a * noise

One-line closed form. t is a batch of timesteps, one per image in the batch.

Step 3: A tiny time-conditioned U-Net

python
import torch.nn as nn
import torch.nn.functional as F
import math
 
def timestep_embedding(t, dim=64):
    half = dim // 2
    freqs = torch.exp(-math.log(10000) * torch.arange(half, device=t.device) / half)
    args = t[:, None].float() * freqs[None]
    emb = torch.cat([args.sin(), args.cos()], dim=-1)
    return emb
 
 
class TinyUNet(nn.Module):
    def __init__(self, img_channels=3, base=32, t_dim=64):
        super().__init__()
        self.t_mlp = nn.Sequential(
            nn.Linear(t_dim, base * 4),
            nn.SiLU(),
            nn.Linear(base * 4, base * 4),
        )
        self.t_dim = t_dim
        self.enc1 = nn.Conv2d(img_channels, base, 3, padding=1)
        self.enc2 = nn.Conv2d(base, base * 2, 4, stride=2, padding=1)
        self.mid = nn.Conv2d(base * 2, base * 2, 3, padding=1)
        self.dec1 = nn.ConvTranspose2d(base * 2, base, 4, stride=2, padding=1)
        self.dec2 = nn.Conv2d(base * 2, img_channels, 3, padding=1)
        self.time_proj = nn.Linear(base * 4, base * 2)
 
    def forward(self, x, t):
        t_emb = timestep_embedding(t, self.t_dim)
        t_emb = self.t_mlp(t_emb)
        t_proj = self.time_proj(t_emb)[:, :, None, None]
 
        h1 = F.silu(self.enc1(x))
        h2 = F.silu(self.enc2(h1)) + t_proj
        h3 = F.silu(self.mid(h2))
        d1 = F.silu(self.dec1(h3))
        d2 = torch.cat([d1, h1], dim=1)
        return self.dec2(d2)

Two-level U-Net with time conditioning injected at the bottleneck. Scale up the depth and width for real images.

Step 4: Training loop

python
def train_step(model, x0, schedule, optimizer, device, T=1000):
    model.train()
    x0 = x0.to(device)
    bs = x0.size(0)
    t = torch.randint(0, T, (bs,), device=device)
    noise = torch.randn_like(x0)
    x_t = q_sample(x0, t, noise, schedule)
    pred = model(x_t, t)
    loss = F.mse_loss(pred, noise)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    return loss.item()

That's the entire training loop: no GAN game, no specialised loss, one MSE call.

Step 5: Sampler (DDPM)

python
@torch.no_grad()
def sample(model, schedule, shape, T=1000, device="cpu"):
    model.eval()
    x = torch.randn(shape, device=device)
    betas = schedule["betas"].to(device)
    sqrt_one_minus_a = schedule["sqrt_one_minus_alphas_cumprod"].to(device)
    sqrt_recip_alphas = schedule["sqrt_recip_alphas"].to(device)
 
    for t in reversed(range(T)):
        t_batch = torch.full((shape[0],), t, dtype=torch.long, device=device)
        eps = model(x, t_batch)
        coef = betas[t] / sqrt_one_minus_a[t]
        mean = sqrt_recip_alphas[t] * (x - coef * eps)
        if t > 0:
            x = mean + torch.sqrt(betas[t]) * torch.randn_like(x)
        else:
            x = mean
    return x

1000 forward passes to produce one batch of samples. In real code, you'd swap this for a DDIM 50-step sampler.

Step 6: DDIM sampler (deterministic, ~20x faster)

python
@torch.no_grad()
def sample_ddim(model, schedule, shape, steps=50, T=1000, device="cpu", eta=0.0):
    model.eval()
    x = torch.randn(shape, device=device)
    alphas_cumprod = schedule["alphas_cumprod"].to(device)
 
    ts = torch.linspace(T - 1, 0, steps + 1).long()
    for i in range(steps):
        t = ts[i]
        t_prev = ts[i + 1]
        t_batch = torch.full((shape[0],), t, dtype=torch.long, device=device)
        eps = model(x, t_batch)
        a_t = alphas_cumprod[t]
        a_prev = alphas_cumprod[t_prev] if t_prev >= 0 else torch.tensor(1.0, device=device)
        x0_pred = (x - torch.sqrt(1 - a_t) * eps) / torch.sqrt(a_t)
        sigma = eta * torch.sqrt((1 - a_prev) / (1 - a_t) * (1 - a_t / a_prev))
        dir_xt = torch.sqrt(1 - a_prev - sigma ** 2) * eps
        noise = sigma * torch.randn_like(x) if eta > 0 else 0
        x = torch.sqrt(a_prev) * x0_pred + dir_xt + noise
    return x

eta=0 is fully deterministic (same noise input always produces the same output). eta=1 recovers DDPM.

Check: Build It0/8
1. precompute_schedule is given betas = torch.tensor([0.1, 0.2]). What is schedule["alphas_cumprod"]?Apply
A code box showing betas set to 0.1 and 0.2, alphas computed as 1 minus betas, and alphas_cumprod as the running product, with the result left blank.
The given betas and the exact precompute_schedule lines from the lesson; the resulting alphas_cumprod is left blank.
2. At some t, sqrt_alphas_cumprod[t] is 0.8 and sqrt_one_minus_alphas_cumprod[t] is 0.6. For one pixel with x0 = 1.0 and noise = -1.0, what does q_sample return?Apply
A code box with sqrt_a 0.8, sqrt_one_minus_a 0.6, x0 1.0 and noise -1.0 substituted into the lesson's q_sample return line, with the result left blank.
The given numbers plugged into the lesson's own q_sample formula; the arithmetic result is left blank.
3. train_step receives a batch x0 of 16 images with T=1000. What does t hold after torch.randint(0, T, (bs,))?Apply
A code box with bs set to 16 and T to 1000, highlighting the line that draws t with torch.randint, with a blank asking for t's shape and values.
The given batch size and T plugged into the lesson's own train_step line; what t looks like is left blank.
4. In the DDPM sample function, what happens on the last loop iteration, when t = 0?Apply
The lesson's DDPM sample loop code with the whole if t greater than 0 block and its else block highlighted together.
The lesson's own sample() code, both branches of the t>0 check shown; which one runs at t=0 is for the question to say.
5. sample_ddim is called with steps=50 and T=1000. How many model forward passes does it make, compared with the sample function at T=1000?Apply
Two code boxes: sample_ddim's for loop over range(steps) with steps=50, and sample's for loop over reversed(range(T)) with T=1000, each making one model call per pass, with the pass counts left as question marks.
Both sampler loops, one model call per pass each; the total number of passes for each is left as a question mark.
6. The lesson's DDIM sampler takes an eta parameter. What do eta=0 and eta=1 each produce?Apply
The lesson's sigma line from sample_ddim, above two cards for eta equals 0 and eta equals 1, each with sigma left blank.
The eta term's own formula; what sigma works out to at eta=0 and eta=1 is left blank.
7. You have a trained DDPM-style model that needs 1000 forward passes per image, and a tight latency budget. Which change fits best?Design
A card listing today's position: a trained epsilon_theta, 1000 forward passes per image, and a tight latency budget, above a dashed box reading change that fits best equals question mark.
The starting position the stem gives, with the change left blank.
8. To compare two model checkpoints fairly, you want the same starting noise to always yield the identical image. Which sampling setup fits?Design
One noise seed box feeding two dashed sampler boxes marked question mark, each producing a run A and run B image, with a note that the two images must be identical.
The requirement the stem states, with the sampler setup left blank.

Use It

For production work, use diffusers:

python
from diffusers import DDPMScheduler, UNet2DModel
 
unet = UNet2DModel(sample_size=32, in_channels=3, out_channels=3, layers_per_block=2)
scheduler = DDPMScheduler(num_train_timesteps=1000)

The library ships ready-made schedulers (DDPM, DDIM, DPM-Solver, Euler, Heun), configurable U-Nets, pipelines for text-to-image and image-to-image, and LoRA fine-tuning helpers.

For research, k-diffusion (Katherine Crowson) has the most faithful reference implementations and the best sampling variants.

Ship It

This lesson produces:

  • outputs/prompt-diffusion-sampler-picker.md, a prompt that picks DDPM / DDIM / DPM-Solver / Euler based on quality target, latency budget, and conditioning type.
  • outputs/skill-noise-schedule-designer.md, a skill that produces a linear, cosine, or sigmoid beta schedule given T and target corruption level, plus diagnostic plots of signal-to-noise ratio over time.
Author's Note

The detail that took me longest to actually believe: the network never predicts the image. It predicts the noise. The first time I saw the loss was just MSE against epsilon, my instinct was that something had to be missing, that the model needed some direct signal about what x_0 looked like. It doesn't, because the forward process is a plain linear equation, so predicting the noise and predicting the image are mathematically interchangeable. Epsilon-prediction just happens to give better-behaved gradients across the full noise range, especially near t = T, where predicting x_0 directly tends to blow up. If you ever train a diffusion model that looks fine at low t and falls apart near the end of the schedule, check the parameterization before you touch the architecture. That bug cost me a week the first time I hit it.

Exercises

  1. (Easy) Visualise the forward process: take one image and plot x_t at t in [0, 100, 250, 500, 750, 1000]. Verify that x_1000 looks like pure Gaussian noise.
  2. (Medium) Train the TinyUNet on the synthetic-circles dataset for 20 epochs and sample 16 circles. Compare DDPM (1000 steps) and DDIM (50 steps) sampling. Do they produce similar images from the same noise seed?
  3. (Hard) Implement a cosine noise schedule (Nichol & Dhariwal, 2021): alpha_bar_t = cos^2((t/T + s) / (1 + s) * pi / 2). Train the same model with linear and cosine schedules and show that cosine gives better samples at low step counts.

Key Terms

TermWhat people sayWhat it actually means
Forward process"Add noise over time"Fixed Markov chain that corrupts an image into Gaussian noise over T steps
Reverse process"Denoise step by step"Learned distribution that walks back from noise to image
Epsilon prediction"Predict the noise"The training target: epsilon_theta(x_t, t) predicts the noise added at step t
Beta schedule"Noise amounts"Sequence of T small variances that define how much noise enters per step
alpha_bar_t"Cumulative retain factor"Product of (1 - beta_s) up to time t; bigger t means less signal left
DDPM sampler"Ancestral, stochastic"Samples each x_{t-1} from its conditional Gaussian; 1000 steps
DDIM sampler"Deterministic, fast"Rewrites sampling as a deterministic ODE; 20-100 steps with similar quality
Time conditioning"Tell the model which t"Sinusoidal embedding of t injected into the U-Net so it knows the noise level

Further Reading