How a network that only ever learns to remove a little noise can create images, video, audio, molecules, and even text. One connected argument from the forward noising process to DDPM, score matching, SDEs, fast samplers, guidance, and flow matching, with every step derived and every claim checked.
Chapter 33 · Section 33.1 · 60 min
Creating by denoising
A diffusion model makes new data, such as an image, by starting from pure noise and removing a little of the noise at a time. Each removal is a small, easy denoising problem, and a thousand of them in a row turn random static into a picture. To learn the removal, we first need the opposite process, the one that adds the noise. This section builds that noising process. We choose it in advance and never learn it, and everything a diffusion model learns, along with every sampler later in the chapter, is defined in terms of it.
This chapter matters because diffusion, together with its close relative flow matching (§33.6), is how most of today's image and video generators work, from Stable Diffusion and FLUX to the video models of §34.5. Most of them run inside the compact latent space of an autoencoder like those of §32.6, and Chapter 34 scales them up. This chapter builds the method itself, on toy data where every quantity can be checked exactly.
Other generators make an image in one network call. A variational autoencoder, or VAE, passes one draw of Gaussian noise through a decoder network and reads off an image (§32.3). Its samples blur, and §32.4 showed why. The decoder is trained with squared error, and the best answer under squared error is the average of all the images that fit the code (§32.1). An average of sharp images is a blurry image.
A generative adversarial network, or GAN, trains its generator against a second network that tries to tell generated images from real ones, the game that §32.6 uses as a loss. Its samples are sharp, but the game is delicate to train, and GANs cover less of the variety in the data than models trained by likelihood (Dhariwal & Nichol 2021). Both ask one network call to do all the work.
Diffusion splits the work into many small pieces. Picture eight small clusters of points arranged on a ring in the plane. Add a little Gaussian noise to every point, then a little more, a thousand times. The clusters swell, merge, and end as one round blob that has forgotten the ring. Now play the movie backward. Each frame differs from the one before it by only a little noise, so each backward step only has to remove a little noise. A network that learns that one small step can apply it a thousand times and turn the blob back into a ring.
This section is about the forward half of the movie, the noising. We define the noising process and find where a point lands after any number of steps. We show that a whole noise schedule is described by one curve, the signal-to-noise ratio. Then we compare the schedules that real systems use, find what breaks when the noise never quite reaches pure noise and when images get larger, and fix one notation for the whole chapter. The next section, §33.2, learns to run the movie backward.
The widget runs the movie forward on the ring. Press Play, or drag the time t from 0, the clean data, to 1000, pure noise. The dashed circle has radius 2, the root-mean-square distance of the points from the center. It is the same at every t, because the process keeps the variance of each coordinate at 1, so the clean ring sits on it and pure noise fills it. The buttons choose the schedule, a recipe for how much noise to add at each step, and the four schedules are defined later in the section. On the right, the top panel shows how much of each original point survives and how large the noise is. The bottom panel shows the log of the ratio of their squares, with the steps where it is below −5 shaded.
How fast does each schedule erase the ring, and where do its steps go?
Eight clusters on a ring, noised over 1000 steps. Left, the points at t. Right, the schedule.
Loading interactive figure…
Why many small steps are easier than one big one
Start with the extreme case, one step from data straight to heavy noise and one step back. Let the data be single numbers, +1 or −1 with equal probability. We corrupt a data point x0 as xt=αx0+σϵ. Here α is the share of the signal that is kept, σ is the size of the noise, and ϵ∼N(0,1) is a standard Gaussian draw. A network trained with squared error to undo this in one shot learns the conditional meanE[x0∣xt]. That is the average of the clean values that could have produced xt, each weighted by how likely it is to have done so. §32.1 proved that it is the best possible guess under squared error.
Under heavy noise, the honest answer to "where did this point come from?" is two spikes, at +1 and −1, with nearly equal weight. That distribution, written q(x0∣xt), is the reverse of the corruption. No single number summarizes two spikes, so the squared-error answer splits the difference. This is the blur of a VAE in its purest form.
Now take a small step instead. Go from time t−1 to time t by shrinking the point slightly and adding a little noise,
xt=1−βtxt−1+βtϵ,ϵ∼N(0,I).
Here βt is a small variance, such as 0.001, and the identity covariance I means that every coordinate gets its own independent noise. Write pt−1 for the density of xt−1, the distribution of all the noisy points one step earlier. The reverse of this small step is nearly Gaussian.
In plain words, to undo a small step, undo the shrink, nudge the point slightly uphill on the log density of the previous frame, and add a little Gaussian noise. The uphill direction ∇logp, the direction in which the density of noisy points rises fastest, is called the score. It runs the chapter from §33.3 on. That a diffusion with small steps can be reversed by steps of the same Gaussian form is a classical result about diffusion processes (Feller 1949). It is the starting point of diffusion models (Sohl-Dickstein et al. 2015).
∂ Why the reverse of a small step is nearly Gaussian
By Bayes rule,
q(xt−1∣xt)=pt(xt)q(xt∣xt−1)pt−1(xt−1).
The denominator does not depend on xt−1, so look at the numerator as a function of xt−1. The first factor is exp(−∥xt−1−βtxt−1∥2/(2βt)) up to a constant. Pull 1−βt out of the norm to get exp(−∥xt−1−m∥2/(2ct)), with m=xt/1−βt and ct=βt/(1−βt). This is a narrow Gaussian bump of width about βt.
Inside that bump a smooth logpt−1 is nearly linear, logpt−1(xt−1)≈logpt−1(m)+g⊤(xt−1−m) with g=∇logpt−1(m). Add the two exponents and complete the square,
The last term does not depend on xt−1, so it goes into the normalizing constant. What remains is the Gaussian N(m+ctg,ctI). The dropped second-order term of logpt−1 changes this by a relative amount of order ct times the curvature, which vanishes as βt→0.
How small is small enough? On the ±1 data we can measure it exactly. Every distribution in sight is then a mixture of two Gaussians, so the true reverse step q(xs∣xt), back to any earlier time s, can be written down. We compare it with the moment-matched Gaussian, the one that has the same mean and variance.
The comparison uses the total variation distance, half the integral of ∣p−q∣. It is the largest difference between the probabilities that the two distributions give to any one event. So 0 means identical and 1 means no overlap at all.
We size a step by how much it changes λ, the log of the ratio of the signal's variance to the noise's variance, which this section defines properly below. Large λ means nearly clean data. At λ=0 the signal and the noise have equal variance, and negative λ means mostly noise. The cell starts from noisy points at λ=0, the hardest place, as we will see. It steps back to λ=0.02, then 0.1, and so on up to 8, which removes almost all the noise, and it averages the distance over 100 noisy points.
The cell needs the exact reverse step. In variance preserving form the signal scale is α=sigmoid(λ) and the noise variance is σ2=1−α2 (both derived below). Write xt=axs+vz with a=αt/αs, v=1−a2, and z a standard Gaussian draw. Given xt and the data point m=±1, xs is Gaussian with precision P=1/σs2+a2/v and mean (αsm/σs2+axt/v)/P. The exact reverse step is these two bumps, weighted by how likely each m is given xt.
Python · cell 1
Starting near λ=0 is the hard case, because there the noisy point could still have come from either data point. Much noisier points come from a distribution that is already nearly Gaussian, so their reverse steps are nearly Gaussian too. Much cleaner points are already decided. From λ=4 the distance is near zero even for a step all the way to λ=8 (try reverse_step_tv(4.0, 8.0)).
The standard schedule's first steps are larger than its average, since λ falls by 0.79 between t=1 and t=2. But they sit near λ=9, where even big steps are harmless for this data (reverse_step_tv(8.42, 9.21) is about 10−13). Real images have fine detail at that scale, which is why §33.2 treats the last reverse step, back to the data itself, separately.
So the reverse process can be learned as a long chain of Gaussian steps. Sohl-Dickstein et al. (2015) proposed exactly this, with a learned mean and variance at each step and a thousand steps for most of their image data. Ho, Jain & Abbeel (2020) fixed the variances, so each step needs only a mean. Learning a mean is regression, fitting a function with squared error. They made the chain produce images that rivaled GANs, under the name denoising diffusion probabilistic models (DDPM). The price is one network call per step, which §33.4 works hard to reduce.
This answers a common interview question. Why are diffusion samples sharp when one-shot generators trained with squared error blur? The reverse of one small step has a single peak, so a Gaussian with a learned mean fits it. The reverse of one big step has several peaks, and a squared-error fit averages them.
The forward process
Everything in this chapter starts from one fixed noising process with no learned parameters. It is a Markov chain, a sequence of random points in which each step depends only on the current point, never on earlier ones. Two letters need care. Here d is the dimension of one data point (the number of pixel values of an image, or 2 for points in the plane), not a model width. And T is the number of noising steps, not a sequence length.
In plain words, at every step, shrink the current point toward the origin by a factor slightly below 1 and add a little independent Gaussian noise. The bold ϵ is always this standard Gaussian noise. A plain ϵ keeps its book-wide meaning of a small constant, and the β of a β-VAE never appears here without a subscript.
Before the general result, here are two steps by hand.
The example generalizes to every t with a short induction.
Proof. By induction on t. For t=1 the definition gives x1=1−β1x0+β1ϵ1. This is the claim with α12=1−β1 and σ12=β1=1−α12. Now suppose xt−1=αt−1x0+σt−1ϵˉ, where ϵˉ∼N(0,I) is built from ϵ1,…,ϵt−1 and so is independent of ϵt and x0. One more step gives
xt=1−βtαt−1x0+(1−βtσt−1ϵˉ+βtϵt).
The coefficient of x0 is 1−βtαt−1=αt, by the definition of αt2 as a product. The bracket is a sum of two independent zero-mean Gaussian vectors with covariances (1−βt)σt−12I and βtI. So it is a zero-mean Gaussian with covariance
So the bracket equals σtϵ for a standard Gaussian ϵ independent of x0, which is the claim at time t. □
In plain words, a thousand small noise injections add up to one big injection. We can jump straight to any time t with one line, xt=αtx0+σtϵ. This is what makes training cheap. To make a training example at time t, there is no need to run t steps of the chain. One draw of ϵ is enough, and §33.2 trains on exactly these pairs. Deriving this marginal is one of the most common diffusion interview questions, so practice it until you can do it at a whiteboard.
The same argument, started at time s instead of 0, gives the jump between any two times. The fast samplers of §33.4 use it to skip many steps at once.
∂ Proof of the jump between two times
Steps s+1,…,t form a chain of the same kind started at xs. So the previous theorem applies with xs in place of x0. It gives the squared signal scale ∏r=s+1t(1−βr)=αt2/αs2 and the noise variance 1−αt2/αs2. The middle form follows from σt2−(αt2/αs2)(1−αs2)=1−αt2/αs2.
For the correlation, write xt=αt∣sxs+σt∣sz with fresh noise z. Given x0, each coordinate has Cov(xs,xt)=αt∣sVar(xs)=αt∣sσs2. Dividing by σsσt gives the claim. Since σt2=αt∣s2σs2+σt∣s2>αt∣s2σs2, the correlation is below 1. □
The marginal fixes the distribution at each time, and the jump fixes how two times are linked. A common shortcut in code gets the first right and the second wrong.
Why the chain shrinks the point before adding noise
Why multiply by 1−βt at all, rather than only adding noise? Track the variance of one coordinate when x0 is random with variance v0. One step gives vt=(1−βt)vt−1+βt, which rearranges to
vt−1=(1−βt)(vt−1−1)⟹vt−1=αt2(v0−1).
So the variance moves geometrically toward 1, and it stays exactly 1 if the data start with variance 1. This is why diffusion models put their data on a scale near 1, images in [−1,1] (standard deviation about 0.5) and latents rescaled to roughly unit variance. Meanwhile the mean αtx0 shrinks to zero. After enough steps xT is close to N(0,I), whatever x0 was. That is the point of the shrink. Sampling has to start from a distribution we can draw from without knowing the data, and the shrink makes that distribution the standard Gaussian.
Without the shrink, xt=xt−1+βtϵt. The variance becomes v0+∑sβs and grows without bound, while the mean stays at x0 forever. The data are never forgotten, only drowned. That process is also used, under the name variance exploding, and we will see shortly that it is the same model in different coordinates.
How close to zero does the signal get? Since log(1−β)=−β−β2/2−⋯≤−β,
αT2=exp(t=1∑Tlog(1−βt))≤exp(−t=1∑Tβt),
with near equality when ∑tβt2 is small.
A noised Gaussian mixture is still a Gaussian mixture
One more closed form, which the rest of the chapter leans on. A Gaussian mixture is a distribution made of several Gaussian bumps, with bump k chosen with probability πk. The ring of the opening is one, with eight bumps. Most of the chapter's toy data are mixtures, because noise keeps them mixtures.
Proof. Condition on the component k. Then αtx0∼N(αtμk,αt2Σk) and σtϵ∼N(0,σt2I) are independent Gaussians. So their sum is Gaussian with the summed mean and covariance. Averaging over k with weights πk gives the mixture. □
Because of this theorem, every quantity that later sections need (the best denoiser, the score, the velocity of a flow) has an exact formula for mixture data. So a sampler can be checked against the truth before any network is trained, and every section of this chapter tests its algorithms that way.
Checking the formula by running the chain
The marginal formula is easy to test. The cell runs the actual thousand-step chain on 20,000 copies of x0=2 with the DDPM schedule. It compares the empirical mean and variance with αtx0 and σt2 at five times.
Python · cell 2
The chain and the formula agree to within sampling error. With 20,000 draws, the typical error is about 0.007 for the mean (the standard deviation divided by 20,000) and 0.01 for the variance. At t=1000 the mean is 2αT=0.013 and the variance is 1, so xT is a standard Gaussian for all practical purposes.
Now take the ring of the opening, standardized so that each coordinate has variance 1, and look at it at five times.
Python · cell 3
At t=50 the clusters are fatter but still separate. By t=200 they have merged into a ring-shaped haze. There αt=0.81, so the noise standard deviation is already 0.58, while neighboring clusters sit 1.08αt=0.88 apart. From t=500 on, the picture is a round Gaussian. A reverse process has to put the eight clusters back, and it does most of that work during the steps where they are still separating.
Core
Noise a batch to per-example times
Implement q_sample(x0, t, alpha, sigma, eps), the line that every diffusion training loop starts with. Each example in the batch is noised to its own time, so example i becomes αtix0,i+σtiϵi.
x0 and eps have shape (B, ...), a batch of vectors (B, d) or of images (B, C, H, W).
t is an integer array of shape (B,), with t[i] the time of example i.
alpha and sigma are arrays of length T+1 that hold αt and σt at index t, so index 0 is t=0, the clean data.
Return an array with the same shape as x0.
import numpy as np
def q_sample(x0, t, alpha, sigma, eps):
return alpha[t] * x0 + sigma[t] * eps # placeholder: broadcasts along the wrong axis
Tests, verified solutions, and AI help, open once you add your API key.
A schedule is one curve, the signal-to-noise ratio
The schedule β1,…,βT looks like a thousand free numbers. It is really one curve. What matters about a noisy point is how much signal it still carries compared with its noise. In the two-step example, after one step the signal had variance 0.9 and the noise 0.1, a ratio of 9. After two steps the ratio was 0.72/0.28=2.57. That ratio, followed over time, describes the schedule completely.
The identity takes one line. From αt2/(1−αt2)=eλt we get αt2=eλt/(1+eλt)=1/(1+e−λt), and σt2=1−αt2=1/(1+eλt). Hoogeboom et al. (2023) use the same identity to define their schedules directly in λ.
In plain words,λt=0 is the moment when signal and noise have equal variance, and every unit of λ is a factor of e in SNR. A variance preserving schedule is completely described by the curve t↦λt. In the two-step example, λ1=log9=2.20 and λ2=log2.57=0.94.
There is a second convention. Instead of shrinking the data, keep it and let the noise grow.
The two look different. One keeps xt near unit scale, and the other lets it grow to σmax. Yet they carry the same information. Take a VP point at λ=0, where αt=σt=0.707 and xt=0.707x0+0.707ϵ. Divide by 0.707 and you get x0+ϵ, a VE point at noise level σ=1=e−0/2.
Proof. Dividing xt=αtx0+σtϵ by αt gives y=x0+(σt/αt)ϵ, and σt/αt=(αt2/σt2)−1/2=e−λt/2. The map xt↦xt/αt is invertible, so observing one is the same as observing the other. Conditioning on either gives the same posterior over x0. □
In plain words, the scale of xt is bookkeeping. Everything a network could learn about x0 from xt depends on t only through λt. So one network can be trained in one convention and sampled in another. §33.4 uses this to rewrite sampling in the VE coordinates of one influential design study, EDM (Karras et al. 2022). §33.3 uses it to treat a VE model, the noise-conditional score networks of Song & Ermon (2019), and the VP model of DDPM as one family. An interviewer who asks "VP or VE?" wants to hear that the information is identical and only the numerics differ.
What the schedule actually controls
If only λ matters, what does choosing a schedule do? It makes two separate choices, and it pays to keep them apart, as Dieleman (2024) argues at length.
First, training draws t uniformly from {1,…,T}. So the schedule decides how often the network practices at each noise level, that is, the distribution of λ it sees in training. For example, the cosine schedule with its small offset set to zero spreads its training over λ with density p(λ)=sech(λ/2)/(2π). That density puts most of the training at moderate ∣λ∣ and very little at the extremes. At λ=0 it is 0.16 per unit of λ, and at λ=±10 it is 0.002.
∂ The density of λ under the cosine schedule
With offset 0, the cosine schedule has αt2=cos2(πu/2) and σt2=sin2(πu/2), with u=t/T. So SNR=cot2(πu/2) and λ=−2logtan(πu/2). Solving for u gives u=π2arctane−λ/2. Differentiating,
Since u is uniform on [0,1], this is the density of λ. It integrates to 1, as a density must.
Second, at sampling time the schedule decides where the reverse steps fall. A training weighting and a sampling grid are different design choices that one schedule happens to tie together. The bound on the likelihood does not care about the shape of the curve. The variational bound is a lower bound on the log-likelihood, the ELBO of §32.2, which §33.2 writes out for diffusion and trains on. In the limit of infinitely many steps it depends on the schedule only through the two endpoints λmin and λmax (Kingma et al. 2021), as that section shows. So the shape of the curve matters only through these two roles, and §33.7 compares the resulting weightings on one axis.
The VP and VE conventions are not the only ones. Rectified flow models move noise to data along straight lines, and they power the open text-to-image model Stable Diffusion 3. They use the linear path xt=(1−t)x0+tϵ for continuous t∈[0,1]. So αt=1−t, σt=t, and λt=2log((1−t)/t). At t=21 the signal and noise scales are both 21, so λ=0 and αt2+σt2=21. The path is neither VP nor VE. Yet by the theorem above it carries the same information as a VP process with the same λt. §33.6 builds its models on this path.
Three schedules, in numbers
Three discrete schedules are the ones you will meet most often in papers and code. We write all three with T=1000.
Linear, from DDPM (Ho et al. 2020). βt rises linearly from β1=10−4 to βT=0.02. The authors chose values that are small next to data scaled to [−1,1], so that each reverse step has nearly the same Gaussian form as the forward step.
Cosine, from a follow-up paper that improved DDPM's likelihoods and sampling speed (Nichol & Dhariwal 2021). It defines the signal directly, αt2=f(t)/f(0) with f(t)=cos2(1+st/T+s⋅2π) and offset s=0.008. Then it sets βt=1−αt2/αt−12 and clips it at 0.999. The offset keeps the first steps from being too small, because the authors found that tiny noise at the start made the network's noise predictions hard to get accurate. With s=0.008 the first step's noise standard deviation, β1=0.0064, sits just below one pixel bin of an 8-bit image scaled to [−1,1], which is 1/127.5=0.0078.
Scaled linear, from Stable Diffusion. βt rises linearly from 0.00085 to 0.012. Stable Diffusion 1 and 2 and its larger successor SDXL use it, in their released scheduler configurations. All three are open text-to-image models that diffuse in the latent space of an autoencoder (§32.1), a compressed grid of numbers that the autoencoder decodes into pixels.
The cell builds all three as arrays indexed by t=0,…,T and prints their key numbers. The next cell plots them.
Python · cell 4
Python · cell 5
Read the plot and the last two columns of the printed table together. The linear schedule reaches equal signal and noise at t=259. It then spends 30 percent of all its steps at λ<−5, where the SNR is below 0.007 and the image is essentially gone. Nichol & Dhariwal (2021) observed the consequence directly. They trained with the linear schedule on ImageNet, a dataset of photos, at 64×64. Then they skipped the first part of the reverse process, its noisiest steps, and measured sample quality with FID, the Fréchet Inception distance. FID compares the statistics of an image classifier's features on generated images and on real ones, and lower is better. Skipping up to 20 percent of the reverse process barely raised FID (their Figure 4), so those steps contribute little.
The cosine schedule was designed so that αt2 falls roughly linearly in the middle and changes slowly at both ends. It reaches SNR 1 near the midpoint, at t=496, and spends only 5 percent of its steps below λ=−5.
The cosine schedule has its own quirk, the cliff at the right end of the λ plot. Without the clip its last step would be βT=1−f(T)/f(T−1)=1, because f(T)=cos2(π/2)=0. Only that last step is clipped. So λ falls from −12.93 at t=999 to −19.84 at t=1000 in a single step. That jump of 6.9 is far larger than any other step of the three schedules, the largest of which is 1.39.
Warm-up
Build the linear and cosine schedules
Implement make_schedule(T, kind) for two kinds of schedule.
"linear", the DDPM schedule, with βt linear from 10−4 to 0.02.
"cosine", the cosine schedule, with offset s=0.008 and βt clipped at 0.999.
Return (alpha, sigma, lam), three arrays of length T+1. Index t holds αt, σt, and λt=log(αt2/σt2) for t=0,…,T. Index 0 is the clean data, with α0=1, σ0=0, and λ0=+∞.
import numpy as np
def make_schedule(T, kind):
raise NotImplementedError
Tests, verified solutions, and AI help, open once you add your API key.
Changing the number of steps
A schedule written as β1,…,βT is tied to its T. Suppose we want the same λ curve, as a function of the fraction t/T, with a different number of steps. Since logαt2≈−∑s≤tβs, the partial sums ∑s≤tβs must stay the same at matching fractions. So each βt must scale like 1/T. Doubling the number of steps halves every beta.
Nichol & Dhariwal (2021) did exactly this when they trained with T=4000. They ran βt from 0.0001/4 to 0.02/4 to keep the shape of αt2. The same picture says that a schedule is really a continuous rate, β(u)=Tβt at u=t/T. That rate is how the chain is written as a stochastic differential equation, its limit as the steps become infinitely many and infinitely small, in §33.3.
Research
How many steps does a schedule need?
This exercise asks how many steps a linear ramp of betas needs to reach a given terminal SNR, and how to change T without changing the curve. Implement three functions. In the first two, target is a terminal SNR between 0 and 1.
approx_min_steps(beta_1, beta_T, target) keeps the endpoints β1 and βT fixed and stretches the ramp over T steps. It returns the first-order estimate of the smallest T with SNR(T)≤target, using SNR(T)≈αT2≈exp(−∑tβt), as an integer rounded up.
min_steps(beta_1, beta_T, target) returns the exact smallest such T. It must agree with a brute-force scan, but it should start from your estimate rather than from 1.
rescaled_linear_betas(T, T_ref=1000, beta_1=1e-4, beta_T=0.02) returns a linear ramp of T betas whose λ curve, as a function of t/T, approximately matches the curve of the linear ramp from beta_1 to beta_T over T_ref steps. Scale both endpoints by one common factor, the one the first-order argument above gives. Here the endpoints change with T.
Tests, verified solutions, and AI help, open once you add your API key.
Terminal SNR, and what breaks when it is not zero
Sampling starts from xT∼N(0,I), pure noise. Training, though, only ever shows the network xT=αTx0+σTϵ, which still contains a little signal unless αT=0. The terminal SNR, SNR(T), measures this gap between what the network learned to expect at t=T and what it is given when sampling.
For DDPM's linear schedule the leak is tiny. The KL divergenceDKL(q∥p) measures how far one distribution is from another, in the expected log-ratio of their densities (§0.5). Ho et al. (2020) report that the KL between the last noisy point q(xT∣x0) and pure noise was about 10−5 bits per dimension, that is, per pixel value. For Stable Diffusion's schedule the leak is not tiny. At t=T,
xT=0.068x0+0.998ϵ.
Lin et al. (2023) traced exactly this failure in Stable Diffusion. It always generates images of medium brightness, and it fails prompts as plain as "Solid black color" or "A white background".
The first fix is a short formula. It keeps α1, sends αT to zero, and stretches everything in between linearly in αt.
Lin et al. report that rescaling in αt, the square root of DDPM's αˉt, preserves the shape of the curve better than rescaling the SNR. For the cosine schedule no rescaling is needed. Deleting the clip already gives βT=1.
A variance exploding process cannot reach zero terminal SNR at all, since σmax is finite. That is one reason Lin et al. recommend the variance preserving form. VE models instead make σmax large compared with the distances between data points, so that the remaining signal is negligible. In an interview, terminal SNR is the standard example of a mismatch between training and testing hidden inside a noise schedule.
Interview
Rescale a schedule to zero terminal SNR
Implement zero_terminal_snr(beta), the rescaling of Lin et al. (2023) defined above. You are given an array of betas β1,…,βT.
Compute αt=∏s≤t(1−βs) for t=1,…,T.
Shift and scale it so that α1 is unchanged and αT=0.
Convert back and return the new betas, an array of length T.
Do not modify the input array.
import numpy as np
def zero_terminal_snr(beta):
beta = beta.copy()
beta[-1] = 1.0 # placeholder: forces alpha_T = 0 but leaves the rest of the schedule alone
return beta
Tests, verified solutions, and AI help, open once you add your API key.
Why bigger images need more noise
A larger image needs more noise to hide the same amount of its content. Take a schedule that works well for 64×64 images and train at 512×512. The same λt now hides much less of the image. Neighboring pixels of a natural image are strongly correlated, and the noise in them is not. So a network that looks at a patch of pixels can average the noise away and still see the layout of the image. Chen (2023) shows noised images at equal noise level and several resolutions, and the large ones are visibly easier to read.
The averaging argument is exact in the simplest case. Let the image be constant, every one of its n pixels equal to c, and noise it as xt,i=αtc+σtϵi. The average of the n noisy pixels is αtc plus noise of standard deviation σt/n. So the SNR of the image's one true number is nαt2/σt2, or in log terms λt+logn. At λt=0 a single pixel has SNR 1, but the average of a flat 64×64 patch has SNR 4,096. For a real image, average-pooling a k×k block leaves the smooth part of the signal nearly unchanged and divides the noise variance by k2.
The cell checks the argument on a smooth synthetic picture rendered at several sizes. The per-pixel SNR is 1 at every size. The cell then average-pools each noisy picture down to 8×8. It computes the SNR of that small picture from the pooled clean signal and the exact pooled noise variance σt2/k2.
Python · cell 6
Without a shift, the coarse SNR grows from 1.0 at 8×8 to 869 at 256×256. That is 322=1024 times the 0.85 of the signal variance that survives pooling. With the shift, it stays near 0.85 for every size above 8×8. A model trained at 256×256 with a schedule tuned at 8×8 would spend nearly all of its training at noise levels where the layout of the image is already obvious. It would barely practice the hard part, deciding the layout from noise.
The widget shows the same effect on a picture. Choose the number of pixels per side and the per-pixel noise scale. The three panels show the clean picture, the noisy one, and the noisy one averaged down to 8×8, roughly what the coarse layout looks like to the network. The big number is the SNR of that 8×8 view. Keep the noise fixed, raise the size, and watch the layout come through and the number grow. The "shift by size" button lowers λ by 2log(n/32), with 32×32 as the reference, and the number then barely changes with size.
Why does the same noise hide less of a larger image?
One picture at n by n pixels, noised pixel by pixel, then averaged down to 8 by 8.
Loading interactive figure…
On the linear path of rectified flow, Stable Diffusion 3 applies the shift to the time itself. For an image of m pixels, a time t tuned for n pixels becomes
tm=1+(α−1)tαt,λtm=λt−2logα,α=m/n.
Here α is the shift, not a signal scale. In plain words, the shift maps each timestep to a later, noisier one, keeps t=0 and t=1 fixed, and lowers the log-SNR by the same constant everywhere. With α=3, the midpoint t=0.5 moves to 0.75.
∂ The timestep shift from the constant-image argument
Esser et al. (2024) derive the shift from the same constant image. Take xt=(1−t)c1+tϵ on n pixels. Dividing the pixel average by 1−t estimates c with standard deviation 1−tt⋅n1.
Ask for equal uncertainty at n pixels and time tn and at m pixels and time tm. That gives 1−tmtm=α1−tntn with α=m/n. Solving for tm gives tm=αtn/(1+(α−1)tn). Since λt=2logt1−t on the linear path, multiplying 1−tt by α lowers λ by exactly 2logα.
The constant image is not a realistic picture, and the authors say so. They pretrained at 256×256, so the formula would give α=4 at 1024×1024. Instead they tried several shifts when sampling from a model trained at 1024×1024 and ran a human preference study. Raters strongly preferred shifts above 1.5 and showed little difference among the larger ones. The authors used α=3.0, a value from that range, for both training and sampling at 1024×1024.
Chen (2023) found the same trend, that the best schedule gets noisier as images get larger because neighboring pixels repeat each other. He also studies a second way to add noise, taken from his group's earlier work. Scale the data by a factor b<1 before noising. That multiplies the SNR by b2, which moves the whole λ curve down by 2log(1/b), the same move as a shifted schedule. The noised input then has variance below 1, so it is renormalized to unit variance before it enters the network.
Latent spaces do not remove the issue. An autoencoder that downsamples by 8 in each direction (§32.1) leaves a latent with less redundancy per element than raw pixels. Yet Stable Diffusion 3 diffuses in such a latent and still needs its shift at high resolution, and latent diffusion returns to this. An interviewer may ask why a model fine-tuned at a higher resolution needs a new schedule. The answer is this averaging argument and the shift by 2log of the size ratio.
Warm-up
The resolution shift for the linear path
Implement three functions, each of which accepts scalars or arrays.
logsnr_linear_path(t) returns the log-SNR λt of xt=(1−t)x0+tϵ.
shift_timesteps(t, shift) returns the Stable Diffusion 3 map t↦αt/(1+(α−1)t), with α = shift.
resolution_shift(m, n) returns the shift α for a schedule tuned at n pixels and used at m pixels.
The tests check numerically that the shift lowers λ by exactly 2logα.
the ϵ target is useless at t=T, so it needs v-prediction
shifted
λ moved down by 2log of the resolution ratio
shifted by a constant
smaller
simple diffusion, Stable Diffusion 3
the shift is tuned by hand, not derived exactly
linear path
αt=1−t, σt=t for t∈[0,1] (not VP)
+∞, −∞
0
Stable Diffusion 3, FLUX.1
trains best with a nonuniform distribution over t
Translating between the papers' notations
Diffusion papers use at least five notations for the same few quantities. The most common bug when implementing a paper is reading its αt as someone else's. The table below translates the papers this chapter uses.
DDIM, a deterministic sampler for the same trained network (Song et al. 2021, §33.4).
VDM, variational diffusion models written in continuous time and log-SNR (Kingma et al. 2021), and DPM-Solver, a fast solver for the sampling equation (Lu et al. 2022).
EDM, the design study that writes everything in the VE noise level (Karras et al. 2022).
This book uses one notation in every section of the chapter, with time running from data at t=0 to noise at t=T (discrete) or t=1 (continuous), flow matching included.
A blank cell means the paper has no separate symbol for that quantity. In EDM the noised sample is x=s(t)x^, where x^ is data plus noise of level σ(t). That is why its scale s(t) plays the role of our αt. The reverse-step variance νt2 is the variance of the noise a sampler adds at each reverse step (§33.2). It gets its own letter because DDPM's and DDIM's σt name that sampling noise, not the forward noise scale.
In PyTorch
Production code keeps the schedule in a small module of float64 buffers. It gathers the values for each example and casts them to the data's dtype only at the end. The block builds the three schedules, noises a batch of latents, applies the zero terminal SNR rescaling, and shows why schedules should not be stored in half precision.
🔥 In PyTorchfor a machine with PyTorch
The cosine schedule's αT2=2.43×10−9 is below the smallest positive float16 number, about 6×10−8. So a schedule stored in half precision silently has αT=0 and λT=−∞. That is exactly the regime where noise prediction breaks, as the terminal SNR part showed. The zero-SNR rescaling leaves λ1 at 7.07 and lowers the middle of Stable Diffusion's curve only slightly (λ500 moves from −0.96 to −1.14). It sends αT from 0.068 to exactly 0.
Read more
Sohl-Dickstein, Weiss, Maheswaranathan & Ganguli (2015). Deep Unsupervised Learning using Nonequilibrium Thermodynamics. The paper that introduced diffusion models, with the small-step argument for why the reverse process has the same Gaussian form.
Ho, Jain & Abbeel (2020). Denoising Diffusion Probabilistic Models. The linear schedule, the closed-form marginal in the notation most code uses, and the results that made the field take diffusion seriously.
Dieleman (2024). Noise schedules considered harmful. A clear essay on why a schedule is really two separate choices, a training weighting over noise levels and a sampling discretization.
Watch next
🎬 But how do AI images and videos actually work? | Guest video by Welch Labs · 3Blue1BrownThe best visual intuition for why many small denoising steps can turn noise into an image, the picture this section formalizes.🎬 What are Diffusion Models? · Ari SeffA short, careful walk through the forward noising chain, its closed-form marginal, and the reverse process, matching this section's derivation and previewing the next.