ml-models / Generative modeling / Diffusion models advanced

Diffusion models

A diffusion model learns to make data by learning to remove noise. This page builds the forward process that turns data into noise in closed form, derives the noise-prediction loss from the ELBO, compares the linear and cosine schedules, samples with DDPM and DDIM, steers samples with classifier-free guidance, and moves the whole thing into a compressed latent space, all worked with an exact denoiser on a one-dimensional dataset.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The forward process in closed form
  5. The ε-prediction objective and the ELBO
  6. Noise schedules
  7. DDPM sampling
  8. DDIM sampling
  9. Classifier-free guidance
  10. Latent diffusion
  11. Worked example
  12. Two schedules, side by side
  13. One posterior step, two ways
  14. A dataset with an exact denoiser
  15. DDPM: a thousand stochastic steps
  16. DDIM: ten deterministic steps
  17. Guidance on a biased condition
  18. How much latent diffusion saves
  19. Implementation
  20. Tradeoffs
  21. Connections
  22. Read first
  23. Read next
  24. References

Problem

A generative model has to turn simple randomness into samples that look like the data. A VAE does it in one decoder pass from a learned latent, and its samples tend to be blurry because the decoder is trained with a simple per-pixel likelihood. A GAN does it in one generator pass and gets sharp samples, but trains as an unstable two-player game that can drop whole modes of the data. An autoregressive model is exact and stable but makes one value at a time.

Diffusion models split the hard jump from noise to data into a long chain of small, easy steps. Each step only has to remove a little noise, which is a plain regression problem with a fixed target, so training is a single stable mean-squared-error loss with no adversary. The cost moves to sampling: making one sample runs the network once per step, tens to a thousand times. This trade, stable training and excellent samples for slow generation, is why diffusion became the standard for images, audio and video.

Intuition

Take a photo and add a little Gaussian noise. Do it again, and again, a thousand times, each time shrinking the image slightly so the values stay bounded. Nothing of the photo is left at the end: it is indistinguishable from a draw of pure noise. This forward process needs no learning, and because every step is Gaussian, the noisy image after any number of steps can be written directly as a mix of the original and one draw of noise.

Now run the film backwards. If a network can look at a noisy image and say which noise was added, subtracting a little of that noise gives a slightly cleaner image. Starting from pure noise and repeating this a thousand times walks a sample back to something that looks like data. The network never has to imagine a whole image at once; at high noise it only decides the rough layout, at low noise only fine texture.

Training is then self-supervised: pick a real image, pick a noise level, add the noise, and ask the network to predict it. The loss is the squared error between the noise it guessed and the noise that was actually added.

The forward and reverse chains Five boxes left to right: x0, the data; x1; x t minus 1; x t; and x T, pure noise. Arrows along the top run left to right and are labelled q, the fixed forward process that adds noise: q of x t given x t minus 1 is a Gaussian with mean the square root of 1 minus beta t times x t minus 1 and variance beta t. A dashed arrow jumps from x0 directly to x t, labelled the closed form. Arrows along the bottom run right to left and are labelled p theta, the learned reverse process, which predicts the noise and removes some of it. x₀ data x₁ xₜ₋₁ xₜ xT noise … … q(xₜ | xₜ₋₁) = N(√(1−βₜ) xₜ₋₁, βₜ I) closed form: xₜ = √ᾱₜ x₀ + √(1−ᾱₜ) ε p_θ(xₜ₋₁ | xₜ): predict ε with ε_θ(xₜ, t), remove a little of it forward: fixed, no parameters · reverse: learned, one network shared by every t

Mechanics

The forward process in closed form

Fix T steps and a noise schedule β1, …, βT, small numbers between 0 and 1. Each step scales the previous value down and adds fresh Gaussian noise:

q(xₜ | xₜ₋₁) = N(√(1 − βₜ) · xₜ₋₁, βₜ I)

Write αt = 1 − βt and ᾱt = α1α2⋯αt. Then the distribution after t steps, given the data, is one Gaussian:

q(xₜ | x₀) = N(√ᾱₜ · x₀, (1 − ᾱₜ) I), so xₜ = √ᾱₜ · x₀ + √(1 − ᾱₜ) · ε with ε ~ N(0, I)

The proof is by induction, using how means and variances move under scaling and adding independent noise. The mean is multiplied by √αt at each step, so after t steps it is √ᾱtx0. The variance follows vt = αtvt−1 + βt; if vt−1 = 1 − ᾱt−1, then vt = αt − ᾱt + 1 − αt = 1 − ᾱt. The factor √(1 − βt) is what keeps the total variance at 1 when the data has variance 1: signal variance ᾱt plus noise variance 1 − ᾱt. Training never runs the chain; it jumps straight to any t with one draw of ε.

The amount of signal left is measured by the signal-to-noise ratio SNR(t) = ᾱt/(1 − ᾱt), which falls from very large at t = 1 to almost zero at t = T. A schedule is good if xT is close enough to N(0, I) that sampling can start there, and if the steps in between spread the work evenly.

The ε-prediction objective and the ELBO

The reverse model is a chain of Gaussians pθ(xt−1 | xt) = N(μθ(xt, t), σt2 I) starting from p(xT) = N(0, I). Like a VAE, with x1, …, xT as latents and the forward process as a fixed encoder, it is trained by minimizing a bound on −log pθ(x0):

L = E_q[ L_T + Σ_{t>1} L_{t−1} + L_0 ], L_{t−1} = KL( q(xₜ₋₁ | xₜ, x₀) ‖ p_θ(xₜ₋₁ | xₜ) )

LT has no parameters, and L0 is a reconstruction term for the last step. The middle terms compare the model with a distribution that is easy to write down: given both the noisy xt and the clean x0, the previous step is Gaussian by Bayes' rule,

q(xₜ₋₁ | xₜ, x₀) = N(μ̃ₜ, β̃ₜ I), μ̃ₜ = (√ᾱₜ₋₁ βₜ / (1 − ᾱₜ)) x₀ + (√αₜ (1 − ᾱₜ₋₁) / (1 − ᾱₜ)) xₜ, β̃ₜ = ((1 − ᾱₜ₋₁) / (1 − ᾱₜ)) βₜ

Substitute x0 = (xt − √(1 − ᾱt) ε)/√ᾱt from the closed form and the mean simplifies to a function of xt and the noise alone:

μ̃ₜ = (1/√αₜ) · (xₜ − (βₜ / √(1 − ᾱₜ)) · ε)

So give μθ the same form with a network εθ(xt, t) in place of ε. Both Gaussians have the same fixed variance, and the KL divergence between two Gaussians with equal variance is their squared mean difference over 2σ2:

L_{t−1} = (βₜ² / (2σₜ² αₜ (1 − ᾱₜ))) · ‖ε − ε_θ(xₜ, t)‖² + const

Every ELBO term is a weighted noise-prediction error. Ho, Jain and Abbeel found that dropping the weight trains better samples, giving the loss everyone uses:

L_simple = E_{t ~ U{1..T}, x₀, ε} ‖ε − ε_θ(√ᾱₜ x₀ + √(1 − ᾱₜ) ε, t)‖²

The weight in front is largest at small t, where the noise is tiny and predicting it is easy; Lsimple sets every weight to 1, which spends relatively more of the training on the large-t steps that decide global structure. It is also a mean squared error against a target that has unit variance at every t, which keeps the network's output scale the same across noise levels. Because a squared error is minimized by the conditional mean, the best possible εθ is E[ε | xt], equivalently the best guess of the clean data, E[x0 | xt].

Noise schedules

The original DDPM schedule is linear: β grows evenly from 10−4 to 0.02 over T = 1000 steps. Nichol and Dhariwal noticed that this destroys the signal too early: by the middle of the chain the image is already almost pure noise, so many steps are spent where there is little to learn. Their cosine schedule defines ᾱ directly,

ᾱₜ = f(t) / f(0), f(t) = cos²( ((t/T + s) / (1 + s)) · π/2 ), s = 0.008

with βt = 1 − ᾱt/ᾱt−1 clipped at 0.999 so the last step is not exactly 1. The small offset s keeps β tiny near t = 0. The cosine curve falls slowly at both ends and roughly linearly in between, so the SNR declines evenly. Later work reparameterizes the schedule entirely by SNR, and models trained with one schedule can be sampled with different step counts, which is where DDIM comes in.

DDPM sampling

To sample, draw xT ~ N(0, I) and apply the learned reverse step T times:

xₜ₋₁ = (1/√αₜ) · (xₜ − (βₜ / √(1 − ᾱₜ)) · ε_θ(xₜ, t)) + σₜ z, z ~ N(0, I), no noise at the last step

DDPM uses σt2 = βt or β̃t, which the paper found to give similar results. Every step injects fresh noise, so the sampler is stochastic and must take all T steps: the reverse Gaussians are only accurate when each step is small. A thousand network evaluations per sample is the main cost of DDPM.

DDIM sampling

Song, Meng and Ermon showed that the same trained network fits a whole family of non-Markovian forward processes that share the marginals q(xt | x0), and the deterministic member of that family can skip steps. At each step, use the predicted noise to estimate the clean data, then re-noise that estimate to an earlier, less noisy level t′ using the same predicted noise:

x̂₀ = (xₜ − √(1 − ᾱₜ) · ε_θ(xₜ, t)) / √ᾱₜ, xₜ′ = √ᾱₜ′ · x̂₀ + √(1 − ᾱₜ′) · ε_θ(xₜ, t)

With no fresh noise, the map from xT to x0 is deterministic: the same starting noise gives the same sample, and nearby starting noises give similar samples, so the starting noise behaves like a latent code that can be interpolated. The step sequence t, t′, … can be any decreasing subsequence of 1…T, often 20 to 100 steps, with no retraining. DDIM also has a knob η that adds back a fraction of DDPM's noise; η = 0 is the deterministic sampler and η = 1 recovers DDPM. Many later samplers treat this update as a step of an ordinary differential equation solver.

Classifier-free guidance

A conditional model εθ(xt, t, c) takes a class label or a text embedding c. To make samples follow c more strongly, Ho and Salimans train one network to be both conditional and unconditional: during training, c is replaced by a null token ∅ for a small random fraction of examples. At sampling time the network runs twice per step and the two predictions are extrapolated:

ε̃ = (1 + w) · ε_θ(xₜ, t, c) − w · ε_θ(xₜ, t, ∅)

w = 0 is the plain conditional model. Larger w pushes away from what the unconditional model predicts, toward what is specific to c. In score terms the guided model follows p(xt | c)1+w / p(xt)w, which is proportional to p(xt | c) · p(c | xt)w: the conditional distribution sharpened by an implicit classifier. Samples become more typical of c and less diverse. Many libraries write the same rule as ε∅ + s · (εc − ε∅) with scale s = 1 + w, so a "guidance scale of 7.5" means w = 6.5.

Latent diffusion

Running a large network a thousand times per sample over every pixel is expensive. Rombach and colleagues first train an autoencoder that compresses an image to a smaller latent, with a perceptual and adversarial loss so that the decoded image keeps its detail, and then train the diffusion model on the latents instead of the pixels. Every formula above is unchanged; x0 is now the encoder's output, and the decoder runs once at the end. With a downsampling factor of 8 and 4 latent channels, a 512 × 512 RGB image becomes a 4 × 64 × 64 latent, 48 times fewer values. Text conditioning enters the denoiser through cross-attention from the latent to the text encoder's token embeddings. This is the design of Stable Diffusion.

ComponentShape/ParamsMeaningSource
Schedule β, ᾱ(T,), T = 1000; linear β from 0.0001 to 0.02How much noise each step adds, and how much signal is leftDDPM paper; cosine: Improved DDPM paper
torch.linspace(start, end, steps)(steps,)The linear β schedule_torch_docs.py
torch.cumprod(input, dim, *, dtype=None, out=None)same shape as inputᾱt as the running product of 1 − β_torch_docs.py
torch.randint(low=0, high, size, *, generator=None)(B,) step indicesOne random t per training example_torch_docs.py
torch.randn_like(input, *, generator=None, dtype=None)same shape as x0The noise ε the network learns to predict_torch_docs.py
torch.randn(*size, *, generator=None)(B, C, H, W)The starting noise xT for sampling_torch_docs.py
Denoiser εθ(xt, t, c)output shaped like xtPredicts the added noise; usually a U-Net or a transformer, with t as an embeddingDDPM paper
F.mse_loss(input, target, reduction="mean", weight=None)scalarLsimple: the squared error between predicted and true noisefunctional.py
torch.manual_seed(seed)returns a torch.GeneratorSeeds every device, so DDIM samples are reproducible from a seedrandom.py
torch.random.fork_rng(devices=None, enabled=True)context managerSamples with a fixed seed without disturbing the global random staterandom.py
Guidance weight wscalar, often 1 to 10Two network calls per step, mixed as (1 + w)εc − wε∅Classifier-Free Guidance paper
Latent autoencoder3 × 512 × 512 → 4 × 64 × 64Diffusion runs on the latent; the decoder maps back onceLatent Diffusion paper

PyTorch v2.15.0 has no diffusion modules; libraries such as Hugging Face Diffusers build schedulers and pipelines from these tensor and random-number operations.

Worked example

All numbers use T = 1000 steps. The schedule part follows one data value x0 = 1 and one noise draw ε = -0.5. The sampling part uses a dataset small enough that the perfect denoiser can be written in closed form, so every sample below is what an ideally trained network would produce.

Two schedules, side by side

tβt (linear)ᾱt linearᾱt cosineSNR linearSNR cosinext (linear)ELBO weight
10.0001000.9999199992.422e+040.9949L₀ term
100.0002790.99810.9994526.815830.97730.0737
500.0010760.9710.99233.5124.10.90030.0186
1000.0020720.8970.97218.7134.830.78670.0101
2500.0050600.52410.8471.1015.5360.37900.0053
5000.0100400.078590.49380.085290.9757-0.19960.0055
7500.0150200.0033510.14430.0033620.1686-0.44130.0077
10000.0200004.036e-052.429e-094.036e-052.429e-09-0.49360.0102

Under the linear schedule the SNR falls below 1 just after t = 250 and is 0.085 at t = 500: the second half of the chain is almost all noise. The cosine schedule keeps the SNR at 0.976 at t = 500, close to an even split. Both end near pure noise: ᾱ1000 is 4.04e-05 for linear and 2.43e-09 for cosine.

The xt column is √ᾱt · 1 + √(1 − ᾱt) · (-0.5). At t = 500 that is √0.0786 − 0.5 · √0.9214 = -0.1996: the value has crossed zero and is mostly the noise. To check the closed form, running all 1000 one-step updates on the mean and variance gives mean 0.006353 and variance 0.999960; the closed form gives √ᾱ1000 = 0.006353 and 1 − ᾱ1000 = 0.999960.

The last column is the ELBO weight βt2/(2σt2αt(1 − ᾱt)) with σt2 = βt. It is 0.0737 at t = 10 and about 0.0053 at t = 250, so the true ELBO counts the near-clean steps over ten times as much as the middle ones. Lsimple replaces every weight by 1.

ᾱ over the chain for the linear and cosine schedules The step t from 0 to 1000 on the horizontal axis and ᾱ t, the fraction of signal variance left, from 0 to 1 on the vertical axis. Both curves start at 1. The linear schedule's curve drops fast, passing 0.52 at t = 250 and 0.08 at t = 500, and is near zero for the whole second half. The cosine schedule's curve falls slowly at first, passes 0.49 at t = 500, and reaches zero only at the very end. 0 250 500 750 1000 0 0.5 1 step t ᾱₜ (signal left) linear β, 0.0001 → 0.02 cosine, s = 0.008 0.079 0.494

One posterior step, two ways

At t = 500 with the linear schedule, xt = -0.1996 from x0 = 1 and ε = -0.5. The posterior coefficients are √ᾱt−1βt/(1 − ᾱt) = 0.00307 on x0 and √αt(1 − ᾱt−1)/(1 − ᾱt) = 0.99411 on xt, so

μ̃ = 0.00307 · 1 + 0.99411 · (-0.19962) = -0.19537

The ε form, (xt − βt/√(1 − ᾱt) · ε)/√αt, gives -0.19537, the same number. A network that predicts ε exactly therefore lands on the true posterior mean. The posterior variance β̃t = 0.010031 is just under βt = 0.010040, which is why DDPM found the two choices of σt2 nearly interchangeable for T = 1000.

A dataset with an exact denoiser

Let the data be x0 = −1 or +1 with equal probability. Given xt, Bayes' rule on the two Gaussians N(±√ᾱt, 1 − ᾱt) gives log odds 2√ᾱtxt/(1 − ᾱt) for +1, so the best possible prediction of the clean value is

E[x₀ | xₜ] = tanh(√ᾱₜ · xₜ / (1 − ᾱₜ)), ε̂ = (xₜ − √ᾱₜ · E[x₀ | xₜ]) / √(1 − ᾱₜ)

This is exactly what minimizing Lsimple converges to with unlimited capacity and data. For a noisy value xt = 0.5 at different steps:

tᾱtE[x0 | xt = 0.5]ε̂
10004.036e-050.00320.5000
7500.0033510.02900.4992
5000.078590.15100.4768
2500.52410.64140.0517
1000.8970.9998-1.3927

At t = 1000 the value says almost nothing about the data, so the best guess of x0 is near the data mean 0 and the predicted noise is the whole value. By t = 100 the guess is 0.9998: the value 0.5 is far closer to √ᾱt · 1 than to −√ᾱt. The prediction is a blurry average at high noise and sharpens as t falls, which is the coarse-to-fine behaviour of real image diffusion.

DDPM: a thousand stochastic steps

Running the DDPM sampler with this exact ε̂ and σt2 = βt, from 1000 draws of xT ~ N(0, 1) with seed 0, puts 51.1% of the samples at +1 and the rest at −1, every one within 5e-15 of a data point. The model reproduces the data distribution, at the cost of 1000 network calls per sample.

DDIM: ten deterministic steps

DDIM with 10 steps visits t = 1000, 900, 800, 700, 600, 500, 400, 300, 200, 100, then jumps to clean data. Five starting values, each run once:

xTafter 5 stepsafter 9 steps10-step result50-step result
-1.5-1.4825-1.2584-1.000000-1.000000
-0.5-0.4944-0.7141-0.999996-1.000000
0.00.00000.00000.0000000.000000
0.50.49440.71410.9999961.000000
1.51.48251.25841.0000001.000000

Each start lands on the data point on its own side, within 10−5 after only 10 network calls, and the same start always gives the same result. The paths hardly move for most of the steps, while the noise still dominates, and commit only in the last few. The start xT = 0 sits exactly on the boundary between the two modes and stays at 0, the average of the data, which is not a data point; that start has probability zero, but in high dimensions the same averaging is what makes samplers with too few steps blurry.

Ten-step DDIM trajectories The DDIM step from 0 to 10 on the horizontal axis and the value from −1.5 to 1.5 on the vertical axis, with dashed lines at the data points −1 and +1. Five trajectories start at −1.5, −0.5, 0, 0.5 and 1.5. The ones starting at ±1.5 and ±0.5 stay almost flat for six steps, then bend in the last three or four steps onto the data point on their own side, ±0.5 first dipping slightly toward zero. The trajectory starting at 0 stays at 0. -1.5 -1 0 1 1.5 0 1 2 3 4 5 6 7 8 9 10 DDIM step (t = 1000 → clean) data +1 boundary data −1

Guidance on a biased condition

Add a condition c whose data is +1 with probability 0.7 and −1 otherwise; the unconditional data stays 50/50. Both exact denoisers are known (the conditional one adds ½ log(0.7/0.3) inside the tanh). Each row runs 400 evenly spread quantiles of N(0, 1) through 50-step DDIM with the guided ε̃ = (1 + w)εc − wε∅:

wscale 1 + wsamples at +1mean samplepc1+w/p∅w, normalized
0170.25%0.40570.0%
1284.75%0.69584.5%
3496.50%0.93096.7%

Without guidance the samples follow the condition's own 70%. With w = 1 they are 84.75% at +1 and with w = 3 96.50%, close to the sharpened distribution pc1+w/p∅w in the last column. Guidance has not produced better samples of the condition; it has produced samples that are more of the condition than the condition's data is. On images that shows up as cleaner, more prototypical pictures with less variety, and at high w as oversaturated colours.

How much latent diffusion saves

A 512 × 512 × 3 image has 786,432 values. With downsampling factor 8 and 4 channels the latent is 4 × 64 × 64 = 16,384 values, 48 times fewer, and every one of the denoiser's tens of calls per sample runs at that size. Self-attention, which costs the square of the number of positions, gains more still: 64 × 64 = 4,096 positions instead of 262,144.

Why can training sample a random t for each example instead of running the chain?

Because q(xt | x0) is a single Gaussian with mean √ᾱtx0 and variance 1 − ᾱt. One draw of ε gives xt for any t directly, and the loss for that t is one term of the ELBO's sum, so a random t per example is an unbiased estimate of the whole sum.

Implementation

Training with label dropout for guidance, and a guided DDIM sampler, for any denoiser called as model(x, t, labels). Labels are class indices, with one extra index reserved as the null label ∅.

import torch
import torch.nn.functional as F

T = 1000
betas = torch.linspace(1e-4, 0.02, T)
alpha_bars = torch.cumprod(1 - betas, dim=0)

def training_loss(model, x0, labels, null_label, p_uncond=0.1):
    t = torch.randint(0, T, (x0.size(0),))
    eps = torch.randn_like(x0)
    ab = alpha_bars[t].view(-1, *[1] * (x0.dim() - 1))       # broadcast over C, H, W
    xt = ab.sqrt() * x0 + (1 - ab).sqrt() * eps
    drop = torch.rand(x0.size(0)) < p_uncond                 # train the unconditional model too
    labels = torch.where(drop, torch.full_like(labels, null_label), labels)
    return F.mse_loss(model(xt, t, labels), eps)

@torch.no_grad()
def ddim_sample(model, shape, labels, null_label, steps=50, w=3.0, generator=None):
    ts = list(range(T - 1, -1, -(T // steps)))[:steps]
    x = torch.randn(shape, generator=generator)
    null = torch.full_like(labels, null_label)
    for k, t in enumerate(ts):
        ab = alpha_bars[t]
        ab_next = alpha_bars[ts[k + 1]] if k + 1 < len(ts) else torch.tensor(1.0)
        tt = torch.full((shape[0],), t)
        eps = (1 + w) * model(x, tt, labels) - w * model(x, tt, null)
        x0_hat = (x - (1 - ab).sqrt() * eps) / ab.sqrt()
        x = ab_next.sqrt() * x0_hat + (1 - ab_next).sqrt() * eps
    return x

Indices here run from 0 to T − 1, one less than the page's t. Real samplers usually clip x̂0 to the data range or rescale it, batch the conditional and unconditional calls into one forward pass of twice the batch, and pass a torch.Generator so a seed reproduces a sample. The T // steps stride is one of several spacings; libraries also offer evenly spaced and "trailing" spacings that include t = T exactly. Some models predict v = √ᾱtε − √(1 − ᾱt)x0 instead of ε (Salimans and Ho's progressive distillation), which behaves better when SNR is near zero; the sampler converts it back to ε and x̂0.

Tradeoffs

  • Versus GANs and VAEs. Diffusion trains with one stable regression loss and covers all the modes of the data; GANs train adversarially and can drop modes but sample in one pass; VAEs sample in one pass and give a likelihood bound but are blurrier. Diffusion pays with tens to a thousand network calls per sample.
  • DDPM vs DDIM. DDPM's injected noise can correct earlier errors and gives the best quality at many steps; DDIM is deterministic, reproducible, invertible to a latent and good at 20 to 100 steps, but degrades faster at very few steps. Distillation and consistency models push to a handful of steps by training a new network.
  • Schedule. Linear is the original and fine at 1000 steps; cosine spends steps more evenly and helps at low resolution and with few sampling steps. At high resolution the same schedule leaves more signal (neighbouring pixels share it), so large models shift the schedule toward more noise.
  • Guidance strength. Higher w gives samples that match the prompt better and look cleaner, at the cost of diversity and, at high w, saturation and artifacts. It doubles the cost of every step.
  • Pixel vs latent. Latent diffusion is many times cheaper and is the default for large images, but the autoencoder caps fine detail such as small text and faces, and is one more model to train.
  • Likelihood. Lsimple gives better samples than the true ELBO; models trained for likelihood need the weights back, learned variances or a different parameterization.
Your DDIM sampler gives the same image every time for the same prompt. Bug?

No: with η = 0 DDIM is deterministic given xT, so the same seed gives the same image. Variety comes from the starting noise; draw a fresh xT (a new seed) for each sample.

Why does classifier-free guidance need label dropout during training?

The guided prediction needs an unconditional εθ(xt, t, ∅) as well as the conditional one. Dropping the label for a fraction of examples trains the same network to make both predictions, so no separate unconditional model or classifier is needed.

Training loss is flat and low but samples are pure noise. What is a likely cause?

A mismatch between training and sampling: a different schedule, t indexed from 0 in one place and 1 in the other, or a model trained to predict x0 or v used as if it predicted ε. The noise-prediction loss can look healthy while the sampler applies the wrong formula at every step.

Connections

References