MIT 6.S184 · Lecture Notes · Holderrieth & Erives

An Introduction to Flow Matching & Diffusion Models

The rigorous reference companion to the five-lecture series — every definition, theorem, and derivation in one place. From "generation is sampling" to the marginalization trick, score matching, classifier-free guidance, diffusion transformers, latent diffusion, and discrete diffusion for language.

Prerequisites: multivariable calculus & the chain rule, linear algebra (vectors, matrices), and basic probability (Gaussians, expectation, Bayes' rule). Comfort with a little Python for the labs. Pairs with the 5 Gleam lectures (fmdm-01 … fmdm-05).
14
Chapters
3
Simulations
3
Code Labs

Chapter 0: What These Notes Build — and Why in One Place

The whole document in one breath. Generative modeling is sampling from an unknown data distribution we only see through examples. The trick that powers Stable Diffusion, Sora, Movie Gen, and AlphaFold3 is to transport an easy-to-sample Gaussian to that data distribution by simulating a differential equation. Flow matching and score matching are the two simulation-free recipes for learning the transport. Guidance steers it toward a prompt. Architectures (U-Net, DiT), latent spaces (VAEs), and a discrete variant (CTMCs for text) make it scale. These notes derive every link in that chain.

The five-lecture Gleam series on this site teaches each idea one lecture at a time, intuition-first, with a single simulation per concept. This Veanor is the rigorous reference companion: it covers the whole set of notes — "An Introduction to Flow Matching and Diffusion Models" by Peter Holderrieth and Ezra Erives (MIT 6.S184) — with the actual definitions, theorems, step-by-step derivations, and notation you would cite. It is still intuition-first; it just refuses to skip a step.

We use the same convention as the notes throughout. Time runs from t = 0 (noise) to t = 1 (data). Objects are vectors x in d-dimensional space. The data distribution is pdata; the easy initial distribution is pinit (almost always a standard Gaussian). A subscript on a function means time-dependence: ut(x) is the vector field at time t and location x.

The seven movements of the notes (and the chapters that cover them).
  • 1. Generation as sampling — objects as vectors, "good" as high density (Ch 1).
  • 2. Flow & diffusion models — ODEs, SDEs, Brownian motion, the Ornstein–Uhlenbeck process, Euler and Euler–Maruyama simulation (Ch 2–3).
  • 3. Flow matching — conditional/marginal probability paths, the marginalization trick, and why the conditional loss has the same gradient as the marginal one (Ch 4–6).
  • 4. Score matching — score functions, denoising score matching, and SDE sampling via the Fokker–Planck equation (Ch 7–8).
  • 5. Guidance — vanilla (classifier) and classifier-free guidance for prompts (Ch 9).
  • 6. Architectures & latent space — time embeddings, the U-Net, the diffusion transformer, VAEs, latent diffusion, Stable Diffusion 3 and Movie Gen (Ch 10–11).
  • 7. Discrete diffusion — continuous-time Markov chains and masked diffusion language models (Ch 12).

Chapter 13 is a one-page cheat sheet and a map to the rest of the site (the 5 Gleam lectures, the Diffusion and Flow Matching Gleams, the Architecture Atlas, and the SDE deep-dive).

How to read this with the lectures. If you have watched a lecture and want the proof, jump to the matching chapter here. If you are starting cold, read top-to-bottom — every symbol is defined before use, every theorem is motivated before it is stated, and the three code labs let you re-derive the central mechanisms (the OU stationary law, the Gaussian conditional score, and classifier-free guidance) with your own hands.
What is the single organizing idea that ties together flow matching, score matching, and discrete diffusion in these notes?

Chapter 1: Generation Is Sampling

Before any differential equation, we need to say precisely what "generate" means. The notes make this move in four key ideas; we restate each with its reasoning.

Objects as vectors

A computer stores numbers, so every object we generate is first flattened to a vector z in Rd (d-dimensional real space). The notes give three concrete encodings:

Key Idea 1 — Objects as Vectors.
  • Image: height H × width W × 3 RGB channels, so z in RH×W×3. A 256×256 photo has d = 256·256·3 ≈ 196,608 numbers.
  • Video: T frames, each an image: z in RT×H×W×3.
  • Molecule: N atoms, each with 3 coordinates: z in R3×N.
Text is the notable exception — it is naturally discrete, which is exactly why Chapter 12 (discrete diffusion) exists.

"Good" means "likely"

There is no single best dog image; there is a spectrum. Machine learning realizes this spectrum as a probability distribution over the space of images, the data distribution pdata — a density pdata : Rd → R≥0 assigning each candidate object a likelihood. A crisp dog photo gets high density; random static gets near-zero density.

Key Idea 2 — Generation as Sampling. The subjective question "is this a good image?" is replaced by the objective question "how likely is it under pdata?" Generating an object becomes drawing a sample:
z ∼ pdata

We only ever have a dataset

The catch — and the reason the entire field exists — is that we do not know the density pdata. Nobody can write down "the probability that this exact arrangement of 196,608 pixels is a real internet image." All we get is a finite pile of samples already drawn from it.

Key Idea 3 — Dataset. A dataset is a finite collection z1, …, zN ∼ pdata drawn independently. As N grows, the dataset becomes a better and better proxy for pdata. Training means squeezing enough information out of these samples to draw new ones.

Conditioning on a prompt

"A dog" and "a cat" are different distributions. A conditioning variable y (a prompt, a class label, a caption) selects a guided data distribution pdata(· | y).

Key Idea 4 — Guided Generation. Guided generation samples z ∼ pdata(· | y), where y can be any prompt. We seek one model that handles arbitrary y. The notes (and we) develop unconditional generation first, then add guidance in Chapter 9 — because the techniques generalize cleanly.

A terminology landmine, flagged now. The word "conditional" gets two meanings in these notes. From Chapter 4 onward, "conditional" almost always means conditioning on a clean data point z (the conditional probability path pt(x|z)). To avoid collision, the notes reserve the word guided for conditioning on a prompt y. We follow that convention strictly.

Why can't we sample pdata directly with a textbook method like inverse-CDF or rejection sampling?

Chapter 2: ODEs, Vector Fields, and Flows

The generative recipe is: pick an easy pinit (the standard Gaussian, written N(0, Id)), then continuously transport a sample of pinit at t = 0 into a sample of pdata at t = 1. "Continuous transport" is the job of an ordinary differential equation (ODE). Three objects describe it from three angles.

Trajectory, vector field, ODE

A trajectory is the path of a single point, a function X : [0,1] → Rd, t ↦ Xt. A vector field assigns a velocity arrow to every location and time:

u : Rd × [0,1] → Rd,    (x, t) ↦ ut(x)

Think of ut(x) as a wind map that can change over the day. The ODE then ties the trajectory to the field: at every instant the trajectory's velocity must equal the arrow at its current location.

d⁄dt Xt = ut(Xt)     (ODE)      X0 = x0     (initial condition)

The first equation says "go where the arrow at your feet points." The second pins down where you start — without it the path is not determined.

The flow — bookkeeping for every start at once. The flow ψt(x0) answers "if I start at x0 and follow the arrows, where am I at time t?" for all starting points simultaneously. It satisfies ψ0(x0) = x0 and d⁄dt ψt(x0) = utt(x0)). A single trajectory is recovered as Xt = ψt(X0).

So vector fields define ODEs whose solutions are flows — three names, one underlying object. The field is the wind; the ODE is the law "drift with the wind"; the flow is the resulting warp of all of space.

A vector field and the trajectory it produces

Blue arrows are the vector field ut(x). The orange dot starts where you click (or press Drop point) and follows the arrows — that traced path is the ODE trajectory. Switch fields to feel how the arrows dictate motion. The "contracting" field u(x) = −θx is the running example we solve by hand below.

field
Click the canvas to drop a point, or press Drop point.

Theorem 3 — existence and uniqueness

Theorem 3 (Flow existence and uniqueness). If ut(x) is continuously differentiable with bounded derivative (more generally, Lipschitz), the ODE has a unique solution given by a flow ψt, and ψt is a diffeomorphism — a smooth, smoothly-invertible warp of space.

Why you can file this away as good news. We will parameterize u with a neural network, and neural networks always have bounded derivatives. So in every case we care about, the flow exists and is unique. Theorem 3 is reassurance, not an obstacle.

Worked example: the linear field, solved exactly

Take the simplest non-trivial field, a contraction toward the origin, ut(x) = −θx with θ > 0. Claim: the flow is ψt(x0) = exp(−θt)·x0. We verify both defining properties:

Step 1 — initial condition. At t = 0: ψ0(x0) = exp(0)·x0 = x0. ✓

Step 2 — the ODE. Using d⁄dt exp(−θt) = −θ exp(−θt) (chain rule):
d⁄dt ψt(x0) = −θ exp(−θt) x0 = −θ ψt(x0) = utt(x0)). ✓

Both hold, so by uniqueness this is the flow. The point decays exponentially to 0.

Plug in numbers: θ = 1, x0 = 2 gives ψt(2) = 2 exp(−t); at t = 1, 2⁄e ≈ 0.7358 — a clean ground truth for testing a numerical integrator.

Simulating an ODE — the Euler method

A neural-net field has no closed-form flow, so we simulate: approximate the journey by many tiny straight steps. The Euler method initializes X0 = x0 and updates

Xt+h = Xt + h·ut(Xt),    h = 1⁄n

where h is the step size and n the number of steps. The derivative is the limit of a difference quotient, d⁄dt Xt ≈ (Xt+h − Xt)/h; setting that equal to ut(Xt) and rearranging gives the update. The arrow is only exactly right at the start of each step, so a finite h leaves an error that shrinks with h — Euler is first-order.

A higher-order step for free — Heun's method. Euler trusts the start-of-step arrow. Heun takes a tentative Euler step to peek at the end-of-step arrow, then steps with the average: first X′ = Xt + h ut(Xt), then Xt+h = Xt + (h/2)[ut(Xt) + ut+h(X′)]. Averaging the two slopes cancels much of the first-order error (Heun is second-order). For this course, plain Euler suffices; real samplers use such schemes to cut network calls.

The flow model and Algorithm 1

A flow model makes two changes to the ODE machinery: the field becomes a neural network uθt with parameters θ, and the start becomes random, X0 ∼ pinit. Randomness enters once, at t = 0; the journey afterward is deterministic. The goal of training is X1 ∼ pdata.

# Algorithm 1 — Sampling from a flow model with the Euler method
# Input: trained vector-field network u_theta(x, t); number of steps n
t = 0.0
h = 1.0 / n                      # step size
x = sample_gaussian()            # X_0 ~ p_init = N(0, I)
for i in range(n):
    x = x + h * u_theta(x, t)    # one Euler step along the learned arrows
    t = t + h
return x                        # X_1, a sample of p_data
The name is misleading — read twice. It is called a flow model, but the network parameterizes the vector field (the arrows), not the flow (the endpoint map). To get a sample you must simulate the ODE. There is no shortcut: sampling = run the integrator for n steps = n network calls. That is why "sampling is slow" is the defining complaint about these models, and why people obsess over fewer, bigger steps.
In a flow model, what does the neural network compute, and what must you do to turn its output into a sample?

Chapter 3: SDEs, Brownian Motion, and the OU Process

Flow models are deterministic after the start. Diffusion models add randomness at every step. The canonical source of continuous randomness is Brownian motion (also called the Wiener process, hence the symbol W — after Norbert Wiener, who taught at MIT).

Brownian motion

A stochastic process is a random trajectory: run it twice from the same start and you get two different paths. Brownian motion W is the continuous-time limit of a random walk.

Definition of Brownian motion. W0 = 0, the paths t ↦ Wt are continuous, and:
  • Normal increments: for 0 ≤ s < t, the jump Wt − Ws is Gaussian with mean 0 and variance equal to the elapsed time, (t − s). Wait longer, spread more — linearly.
  • Independent increments: jumps over non-overlapping intervals are statistically independent — the future increment ignores the past path.

To simulate, discretize time into steps of size h and add a Gaussian kick whose variance is h. Since standard normal noise has variance 1, scaling by √h gives variance h:

Wt+h = Wt + √h · εt,    εt ∼ N(0, Id)
The √h is the whole subtlety — never write h. Variances add over independent steps. After n = t/h steps the total variance is (t/h)·h = t — exactly the law Var[Wt] = t. If you wrongly scaled by h (variance per step), total variance would be (t/h)·h² = t·h → 0: the path would freeze. The square root makes the continuous limit non-degenerate. (Brownian paths are continuous but infinitely long — you could draw one without lifting your pen, yet never finish.)

From ODEs to SDEs

An SDE is an ODE with Brownian noise stirred in. Because a random path is too jagged to differentiate, we first rewrite the ODE without derivatives. Using the difference-quotient definition:

d⁄dt Xt = ut(Xt)  ⇔  (1/h)(Xt+h − Xt) = ut(Xt) + Rt(h)
⇔  Xt+h = Xt + h·ut(Xt) + h·Rt(h)

where Rt(h) → 0 as h → 0. This restates Euler with no derivatives. Now amend it with a Brownian kick scaled by a diffusion coefficient σt ≥ 0:

Xt+h = Xt + h·ut(Xt) [deterministic] + σt(Wt+h − Wt) [stochastic] + h·Rt(h)

This is the Euler–Maruyama update. Since the Brownian increment is √h·ε, in code it reads Xt+h = Xt + h·ut(Xt) + σt·√h·ε. Mathematicians write the same thing in compact symbolic form (shorthand for the limit of the update above):

dXt = ut(Xt) dt + σt dWt

Here ut is the drift (deterministic pull) and σt the diffusion (noise strength). Two facts keep SDEs friendly:

Two key facts.
  • Every ODE is an SDE with σt = 0. Turn off the noise and you recover deterministic flow. So we can speak of SDEs and treat ODEs as the noiseless corner.
  • Theorem 5 (existence & uniqueness): if the drift is Lipschitz and σt is continuous, a unique solution process exists. But there is no flow map anymore — Xt is not determined by X0 alone, because the path keeps rolling dice.

The Ornstein–Uhlenbeck process

The cleanest non-trivial SDE is the Ornstein–Uhlenbeck (OU) process: constant linear drift ut(x) = −θx (a spring toward 0, θ > 0) and constant diffusion σ.

dXt = −θXt dt + σ dWt

Two forces fight: the drift pulls in, the noise pushes out. Their balance produces a stationary distribution — a steady-state spread the process settles into and never leaves:

Xt ⟶[t→∞] N(0, σ²⁄(2θ))

Sanity-check the extremes. More noise σ → wider spread (σ² on top). Stronger pull θ → tighter spread ( on the bottom). If σ = 0, the variance is 0 — every path collapses to the origin, recovering the contracting flow from Chapter 2.

Worked number. Take θ = 0.5, σ = 1. Stationary variance = σ²⁄(2θ) = 1⁄(2·0.5) = 1.0 — standard deviation 1.0. We verify this empirically in the lab.
Why OU matters for real diffusion. Run OU forward from any data point and it forgets everything, decaying to the simple Gaussian N(0, σ²/2θ) — this is the "noising"/forward process that turns data into noise. Diffusion models learn to run an SDE that reverses it: start from the easy Gaussian and drift back toward data. The OU process is the textbook noising process; the training problem (Chapters 6–8) is learning the drift that undoes it.

Now assemble everything: implement the OU drift and the full Euler–Maruyama step, then verify the stationary variance.

For the OU process, increasing θ while holding σ fixed does what to the stationary distribution, and why?

Chapter 4: Flow Matching — Conditional & Marginal Probability Paths

Chapters 2–3 built the machine (ODEs/SDEs and how to simulate them) but assumed the vector field was handed to us. Flow matching is the algorithm for training the field so that simulating the ODE produces samples from pdata. We restrict to flow models: X0 ∼ pinit, dXt = uθt(Xt) dt, and we want X1 ∼ pdata. The question "how to train" is really "how do we pick θ so the endpoint lands on the data?"

Step 1 — specify a probability path

The trajectory must satisfy X0 ∼ pinit and X1 ∼ pdata, but we have freedom in what distribution the points pass through at intermediate times. A probability path formalizes that freedom — a trajectory in the space of distributions.

Let δz denote the Dirac delta at z: sampling it always returns z (it is the deterministic "all mass at one point" distribution). A conditional (interpolating) probability path is a family of distributions pt(x|z) with

p0(·|z) = pinit,    p1(·|z) = δz    for all z

In words: conditioned on a fixed data point z, the path gradually converts noise into a spike on that exact z. (This is why we call it conditional — conditioned on the data point, not the prompt.)

The marginal probability path. Every conditional path induces a marginal probability path pt — the distribution you get by first drawing a real data point z ∼ pdata, then sampling x ∼ pt(·|z):
pt(x) = ∫ pt(x|z) pdata(z) dz
We can sample pt (draw z, then draw from pt(·|z)), but we cannot evaluate its density — that integral over all data points is intractable. Hold onto this asymmetry; it drives the whole derivation.

Because of the endpoint conditions on pt(·|z), the marginal path automatically interpolates noise and data: at t = 0 every conditional path is pinit, so p0 = pinit; at t = 1 each conditional is δz, so the mixture over z ∼ pdata reassembles p1 = pdata. That is the entire point.

The Gaussian probability path (the one that matters)

Almost all state-of-the-art models use the Gaussian probability path. Choose two noise schedulers αt, βt — continuously differentiable, monotonic functions with α0 = β1 = 0 and α1 = β0 = 1 — and define

pt(·|z) = N(αt z, βt2 Id)

Check the endpoints: at t = 0, N(0·z, 1·I) = N(0, I) = pinit; at t = 1, N(1·z, 0·I) = δz (a Gaussian with zero variance and mean z is the spike at z). Both conditions hold, so it is a valid conditional path. Sampling the marginal Gaussian path is the one-liner everyone implements:

z ∼ pdata,   ε ∼ N(0, Id)  ⇒  x = αt z + βt ε ∼ pt

This is just "take a data point, scale it down by αt, and add Gaussian noise of scale βt." More noise for lower t, until at t = 0 there is only noise.

Conditional vs. marginal probability path (Gaussian, αt = t, βt = 1 − t)

Drag the time slider. Conditional mode shows pt(·|z) for one fixed data point z (orange star): a single Gaussian blob that starts wide at t = 0 and contracts onto z at t = 1. Marginal mode mixes over several data points (a ring of stars): the cloud starts as one Gaussian and splits into the data distribution. Same path, two views — exactly Figure 5 of the notes.

time t0.00
view
For the Gaussian conditional path pt(·|z) = N(αtz, βt2I), why is the marginal density pt(x) intractable even though we can sample it easily?

Chapter 5: Conditional & Marginal Vector Fields — The Marginalization Trick

A probability path says what distribution the points should have at each time. But "should" is a wish — we need an actual vector field whose ODE trajectories realize it. Flow matching constructs one explicitly.

Conditional vector field

For each data point z, a conditional vector field utargett(x|z) is any field whose ODE follows the conditional path:

X0 ∼ pinit,   d⁄dt Xt = utargett(Xt|z)  ⇒  Xt ∼ pt(·|z)

We can usually write this down by hand. For the Gaussian path, build the conditional flow first: ψtargett(x|z) = αt z + βt x. If X0 ∼ N(0,I), then Xt = αt z + βt X0 ∼ N(αtz, βt2I) = pt(·|z) — correct distribution. To extract the field, use the flow defining property and the chain rule (writing α̇t, β̇t for time derivatives):

d⁄dt ψtargett(x|z) = α̇t z + β̇t x, and this must equal utargetttargett(x|z)|z) = utargetttz + βtx | z).

Reparameterize x → (x − αtz)/βt on both sides and collect terms →
utargett(x|z) = (α̇t − (β̇ttt) z + (β̇tt) x
a clean linear function of x and z. For the CondOT schedulers αt = t, βt = 1 − t (so α̇t = 1, β̇t = −1) this collapses to the famous utargett(x|z) = (z − x)/(1 − t), equivalently z − ε along the sampled path.

Why a conditional field looks useless — and is not. Following utargett(·|z) always lands on the same fixed z — you would just re-generate a known data point. The payoff is that conditional fields are building blocks for a field that generates genuinely new samples.

Theorem 9 — the marginalization trick

Theorem 9 (Marginalization trick). Given conditional vector fields utargett(x|z), define the marginal vector field as a posterior-weighted average:
utargett(x) = ∫ utargett(x|z) · [pt(x|z) pdata(z) ⁄ pt(x)] dz
Then its ODE follows the marginal path: X0 ∼ pinit, dXt = utargett(Xt) dt ⇒ Xt ∼ pt. In particular X1 ∼ pdata — the marginal field "converts noise into data."

Read the weight. By Bayes' rule, pt(x|z) pdata(z) ⁄ pt(x) is the posterior over clean data points z given the noisy point x — "given I see this noisy x at time t, how plausible is it that it came from z?" So the marginal field is intuitive: for every candidate z it takes the velocity that would steer toward z, and averages those velocities, weighting each by how much we believe x came from that z.

The continuity equation and the proof

The proof rests on the continuity equation, the conservation law of probability mass. Define the divergence div(vt)(x) = ∑i ∂vit/∂xi (the net outflow of the field at a point).

Theorem 11 (Continuity Equation). For a flow with field utargett and X0 ∼ p0, we have Xt ∼ pt for all t if and only if
t pt(x) = −div(pt utargett)(x)
Intuitively: the rate the density changes at x equals the net inflow of probability mass. Divergence measures net outflow, so −div measures inflow; mass is conserved, so the two sides match.

Proof of Theorem 9. By Theorem 11 it suffices to show the marginal field satisfies the continuity equation. Direct calculation, with each step labeled:

(i) t pt(x) = ∂t ∫ pt(x|z) pdata(z) dz = ∫ ∂t pt(x|z) pdata(z) dz  (differentiate inside the integral)

(ii) = ∫ −div(pt(·|z) utargett(·|z))(x) pdata(z) dz  (continuity eq. for each conditional path)

(iii) = −div(∫ pt(x|z) utargett(x|z) pdata(z) dz)  (swap div and integral)

(iv) = −div(pt(x) ∫ utargett(x|z) [pt(x|z)pdata(z)/pt(x)] dz)  (multiply and divide by pt(x))

(v) = −div(pt utargett)(x)  (definition of the marginal field)

The first and last lines are precisely the continuity equation for utargett. By Theorem 11 this implies Xt ∼ pt, finishing the proof. (The Fokker–Planck equation in Chapter 8 generalizes Theorem 11 to SDEs.)

In the marginal vector field, what is the role of the weight pt(x|z) pdata(z) / pt(x)?

Chapter 6: The Flow Matching Loss — Conditional = Marginal in Gradient

We now have the training target: make the network uθt equal the marginal vector field utargett. If it does, then by Theorem 9 the endpoints X1 ∼ pdata are exactly what we want. The obvious objective is a mean-squared error, the flow matching loss (with Unif the uniform distribution on [0,1] and E expectation):

LFM(θ) = Et∼Unif, x∼pt [ ‖ uθt(x) − utargett(x) ‖2 ]
The wall. We have a formula for utargett (Theorem 9), but it contains the intractable integral over pdata — we cannot evaluate it, so we cannot compute this loss. The fix is the central idea of flow matching.

The conditional flow matching loss

The conditional field utargett(x|z) is tractable (we wrote it in closed form in Chapter 5). So define the conditional flow matching loss, regressing against the conditional field instead:

LCFM(θ) = Et∼Unif, z∼pdata, x∼pt(·|z) [ ‖ uθt(x) − utargett(x|z) ‖2 ]

This is computable: draw a data point z, draw a time t, sample x ∼ pt(·|z) (just noise z), evaluate the network, take an MSE against the known conditional velocity. But why should regressing the conditional field give us the marginal field we actually care about?

Theorem 12 — the two losses share a gradient

Theorem 12. The two losses differ only by a constant in θ:
LFM(θ) = LCFM(θ) + C,    ∇θ LFM(θ) = ∇θ LCFM(θ)
So minimizing the (tractable) conditional loss by SGD is identical, gradient-for-gradient, to minimizing the (intractable) marginal loss. At the minimizer, uθt = utargett (with an infinitely expressive network).

Proof sketch (the load-bearing step). Expand the MSE with ‖a−b‖2 = ‖a‖2 − 2a·b + ‖b‖2. The ‖utarget2 term has no θ, so it is a constant C1. The ‖uθ2 term is identical under both sampling schemes (sampling x ∼ pt equals sampling z then x ∼ pt(·|z)). The only term that mixes the network with the target is the cross term, and there the magic happens:

Et,x∼pt[uθt(x)·utargett(x)]
= ∫∫ pt(x) uθt(x)·utargett(x) dx dt  (expectation as integral)
= ∫∫ pt(x) uθt(x)·[∫ utargett(x|z) pt(x|z)pdata(z)/pt(x) dz] dx dt  (plug in Theorem 9)
= ∫∫∫ uθt(x)·utargett(x|z) pt(x|z)pdata(z) dz dx dt  (the pt(x) cancels!)
= Et,z∼pdata,x∼pt(·|z)[uθt(x)·utargett(x|z)]

The intractable pt(x) in the denominator cancels the pt(x) from writing the expectation as an integral — the cross term, started with the marginal field, ends as an expectation over the conditional field. Substituting back and re-completing the square gives LFM = LCFM + C. Since C is independent of θ, the gradients coincide. □

Algorithm 3 — flow matching training

For the Gaussian path, the conditional field is utargett(x|z) = (α̇t − (β̇ttt)z + (β̇tt)x, and substituting x = αtz + βtε simplifies the loss to a regression against α̇tz + β̇tε. For the CondOT path αt = t, βt = 1 − t, that target is just z − ε:

# Algorithm 3 — Flow Matching Training (Gaussian CondOT path p_t(x|z) = N(t·z, (1-t)^2))
for each mini-batch in dataset:
    z   = sample_data()              # a real data point z ~ p_data
    t   = uniform(0, 1)              # random time
    eps = sample_gaussian()          # noise ~ N(0, I)
    x   = t*z + (1 - t)*eps          # x ~ p_t(·|z): noise the data point
    loss = norm(u_theta(x, t) - (z - eps))**2   # regress against z - eps
    grad_update(loss)
Three striking features of Algorithm 3.
  • Simulation-free: training never rolls out an ODE — no integration loop, so it is cheap. (You only simulate at inference.)
  • Just regression: it is a mean-squared error against a known target — not far from ordinary supervised learning.
  • Dead simple: noise a data point, predict the velocity. It is hard to imagine a simpler objective.
Stable Diffusion 3 and Meta's Movie Gen Video are trained with exactly this procedure (Chapter 11).
Theorem 12 says we may train on LCFM instead of LFM. What makes this not just an approximation but exactly correct for optimization?

Chapter 7: Score Functions — the Conditional & Marginal Score

So far our central object was the vector field ut(x). Diffusion models take a different but equivalent perspective built on score functions. This chapter rephrases everything in that language — and shows the two are linked by a single conversion formula.

The score function

For any distribution q(x), the score function is the gradient of the log-density, ∇ log q(x). It points in the direction of steepest ascent of log-likelihood — "which way should I nudge x to make it more probable?" At a mode it is zero; in the tails it points back toward the bulk.

Returning to probability paths, define the conditional score ∇ log pt(x|z) and the marginal score ∇ log pt(x). They are related just like the vector fields:

∇ log pt(x) = ∫ ∇ log pt(x|z) · [pt(x|z) pdata(z) ⁄ pt(x)] dz

The same posterior weight appears. The derivation uses y log y = 1/y and the chain rule: ∇ log pt(x) = ∇pt(x)/pt(x) = [∫ ∇pt(x|z)pdata(z)dz]/pt(x), and writing ∇pt(x|z) = pt(x|z)∇ log pt(x|z) gives the weighted average.

The Gaussian conditional score

For the Gaussian path pt(x|z) = N(αtz, βt2I), the log-density is −‖x − αtz‖2/(2βt2) plus a constant in x. Its gradient is the clean linear form:

∇ log pt(x|z) = −(x − αtz) ⁄ βt2

Notice: the score points from the noisy point back toward the (scaled) clean point, scaled by 1/βt2. Since x = αtz + βtε, this equals −ε/βt — the score is, up to scale, the negative of the noise that was added. That is why learning the score and "predicting the noise" are the same task (next chapter).

Proposition 1 — field and score are interchangeable

Proposition 1 (Conversion for Gaussian paths). Because both the conditional field and the conditional score are linear in x and z, they convert into each other:
utargett(x|z) = at ∇ log pt(x|z) + bt x,    at = (βt2 α̇tt − β̇tβt),   bt = α̇tt
and the identical relation holds for the marginal field and marginal score: utargett(x) = at ∇ log pt(x) + bt x.

This is striking: once you have learned the marginal vector field, you have also learned the score — and vice versa. Many diffusion models therefore learn the score instead of the field; for Gaussian paths it makes no difference. (To carry the conditional identity to the marginal one, integrate against the posterior weight and use that the posterior integrates to 1.)

The denoiser (posterior mean)

A third equivalent quantity is the denoiser Dt(x) = E[z | x] — the expected clean data point given the noisy x. Field, score, and denoiser are all linear reparameterizations of this posterior mean, so any one recovers the others. Predicting the denoiser is often preferred for numerical stability; models that do are called denoising diffusion models.

Now confirm the Gaussian conditional-score formula numerically — verify the analytic gradient against a finite-difference of the actual log-density, with no spoiler of the formula in the check.

For the Gaussian path, Proposition 1 says the conditional vector field and conditional score are interchangeable. Why is that possible specifically here?

Chapter 8: Score Matching & Sampling with SDEs

Two jobs remain. First, learn the marginal score directly (for paths where we cannot just convert from the field). Second, turn the deterministic ODE sampler into a stochastic SDE sampler — which often produces better, self-correcting samples.

Denoising score matching

To approximate ∇ log pt(x), use a score network sθt. As with flow matching there is an intractable score matching loss (regress against the marginal score) and a tractable denoising score matching loss (regress against the conditional score):

LSM(θ) = E[ ‖ sθt(x) − ∇ log pt(x) ‖2 ],    LCSM(θ) = E[ ‖ sθt(x) − ∇ log pt(x|z) ‖2 ]
Theorem 22. Exactly as Theorem 12, LSM(θ) = LCSM(θ) + C with C independent of θ, so the gradients coincide and the minimizer satisfies sθt = ∇ log pt. The proof is identical — the marginal-score formula has the same shape as the marginal-field formula, so swap utargett for ∇ log pt and the same pt(x) cancellation goes through.

For the Gaussian path, plugging ∇ log pt(x|z) = −ε/βt into LCSM shows the network is learning to predict the noise that corrupted the data. This is unstable as βt → 0 (the 1/βt2 blow-up you saw in the lab), so DDPM dropped the constant and reparameterized into a noise predictor εθt:

LDDPM(θ) = Et, z∼pdata, ε∼N(0,I) [ ‖ εθttz + βtε) − ε ‖2 ]

This is the loss behind the original Denoising Diffusion Probabilistic Models. Algorithm 4 is Algorithm 3 with "predict the velocity" replaced by "predict the noise" — noise a data point, regress against ε.

Theorem 17 — the SDE extension trick

The marginal field gives an ODE that follows the path. Can we follow the same marginal path with an SDE? Yes — and we get to choose the noise level after training.

Theorem 17 (SDE extension trick). For any diffusion coefficient σt ≥ 0, the SDE
dXt = [ utargett(Xt) + (σt2/2) ∇ log pt(Xt) ] dt + σt dWt
with X0 ∼ pinit has marginals Xt ∼ pt, hence X1 ∼ pdata. The extra t2/2)∇ log pt drift is a score-correction that exactly compensates for the noise you inject, so the marginal distribution is unchanged.

Why this is remarkable. One trained model gives you an entire family of samplers indexed by σt. At σt = 0 you recover the deterministic probability-flow ODE; at σt > 0 you get stochastic sampling that injects fresh noise (the score term steers it back), which can self-correct accumulated errors. In theory any σt works; in practice training error and finite step sizes mean there is an empirically best σt.

The Fokker–Planck equation (why it works)

The proof generalizes the continuity equation to SDEs. Define the Laplacian Δwt(x) = ∑i2wt/∂xi2 = div(∇wt)(x).

Theorem 19 (Fokker–Planck Equation). For the SDE dXt = ut(Xt)dt + σt dWt, we have Xt ∼ pt for all t iff
t pt(x) = −div(pt ut)(x) + (σt2/2) Δpt(x)
The first term is the continuity equation (transport); the new Laplacian term is exactly the diffusion (heat-equation) term — noise spreads mass like heat. At σt = 0 it collapses back to Theorem 11.

Proof of Theorem 17 (sketch). Start from the continuity equation for the ODE field, t pt = −div(pt utargett). Add and subtract t2/2)Δpt. Rewrite one copy using Δpt = div(∇pt) and ∇ log pt = ∇pt/pt, so t2/2)Δpt = div(pt·(σt2/2)∇ log pt). Folding it into the divergence gives t pt = −div(pt[utargett + (σt2/2)∇ log pt]) + (σt2/2)Δpt — the Fokker–Planck equation for the SDE in Theorem 17. By Theorem 19, Xt ∼ pt. □

Langevin dynamics — the static special case. If the path is constant, pt = p, set utargett = 0 to get dXt = (σt2/2)∇ log p(Xt)dt + σt dWtLangevin dynamics. Since tp = 0, p is its stationary distribution, and the chain converges to p from arbitrary starts. This is the backbone of MCMC and molecular dynamics; the OU process is the Gaussian special case.
Theorem 17 lets you add a noise term σt dWt to a trained flow model and still hit pdata. What keeps the marginal distribution unchanged when you inject that noise?

Chapter 9: Guidance — Vanilla & Classifier-Free

Everything so far was unguided: the model returns some sample of pdata. We almost always want a prompt: sample pdata(·|y) for a text prompt or class y. Recall the terminology: "conditional" means conditioning on a clean data point z; guided means conditioning on a prompt y.

Vanilla guidance

The simplest approach: feed y to the network as an extra input, uθt(x|y), and train exactly as before but with data pairs (z, y) ∼ pdata. The guided conditional flow matching objective is

LguidedCFM(θ) = E(z,y)∼pdata, t, x∼pt(·|z) [ ‖ uθt(x|y) − utargett(x|z) ‖2 ]

The only change from the unguided loss is sampling (z,y) instead of just z — in PyTorch, a dataloader returning both images and prompts. The prompt does not change the conditional path or conditional field; it just enters the network. In theory this gives faithful samples from pdata(·|y).

Why vanilla guidance is not enough. Empirically, vanilla samples often ignore the prompt — "corgi dog" yields a generic dog. Reasons: the model underfits the true guided field, or the text–image pairs scraped from the web are noisy. To force adherence we must artificially amplify the influence of y.

Classifier guidance (the stepping stone)

For Gaussian paths, write the guided field via the guided score, utargett(x|y) = at ∇ log pt(x|y) + bt x. By Bayes' rule (gradient in x, so ∇ log pt(y) = 0):

∇ log pt(x|y) = ∇ log pt(x) + ∇ log pt(y|x)

Substituting gives utargett(x|y) = utargett(x) + at ∇ log pt(y|x): the guided field is the unguided field plus the gradient of a classifier log pt(y|x) (the log-likelihood of the prompt given the noisy image). Classifier guidance scales that term by a guidance scale w > 1:

ũt(x|y) = utargett(x) + w · at ∇ log pt(y|x)

It works, but needs a second network (a classifier on noisy data), and for high-dimensional y (text) pt(y|x) is hard to learn. Note w ≠ 1 makes this a heuristic — it is no longer the true guided field.

Classifier-free guidance (CFG)

The fix that runs every modern image/video model: get the same amplifying effect without a separate classifier. Substitute the Bayes identity ∇ log pt(y|x) = ∇ log pt(x|y) − ∇ log pt(x) into the classifier-guidance formula and collect terms. The score and the btx pieces recombine into whole vector fields, leaving the clean linear combination:

Summary 27 — Classifier-Free Guidance.
ũt(x|y) = (1 − w) utargett(x|∅) + w · utargett(x|y)
The guided field is an extrapolation away from the unguided field utargett(x|∅) through the guided field utargett(x|y). At w = 1 it is exactly the true guided field; for w > 1 it overshoots toward the prompt. This is valid for any probability path, not just Gaussian.

One model, not two. The trick is to augment the label set with a special null label meaning "no conditioning," and set utargett(x) = utargett(x|∅). During training (Algorithm 5) we drop the real label to with probability η, so the same network learns both the guided and unguided fields. At inference we evaluate both branches and combine them with the formula above.

Classifier-free guidance scale — one model, dial the prompt strength

The teal dot is the unguided target (population center, prompt off); the orange star is the guided target for prompt y. The purple marker is the CFG result (1−w)·unguided + w·guided. Drag w: at w = 1 the result sits on the guided target; as w grows it extrapolates past it, sharpening prompt adherence (and eventually over-saturating). Almost every AI image you see uses w ≥ 4.

guidance w1.0

Now build the CFG combination on a 2D toy and confirm the geometry: at w = 1 you recover the guided field; larger w pushes strictly further toward the prompt.

Inference and caveats. Sample by simulating dXt = ũθt(Xt|y) dt from X0 ∼ pinit (or the SDE version, swapping uθt for ũθt). For w > 1 the output is no longer distributed exactly as pdata(·|y) — CFG is a heuristic justified by excellent empirical prompt adherence, not by a distributional guarantee.
Classifier-free guidance avoids training a separate classifier. How does one network supply both the unguided field utargett(x|∅) and the guided field utargett(x|y)?

Chapter 10: Architectures & Time Embeddings

The math above never said how the network uθt(x|y) is built. It has three inputs — a vector x in Rd, a prompt y, and a scalar time t — and one output, a vector uθt(x|y) in Rd of the same shape as x (it is a velocity per coordinate). For toy distributions an MLP over the concatenation of x, y, t works. For images and video we need specialized architectures.

Embedding the conditioning inputs

Time. Concatenating the raw scalar t works for toys, but in practice t is lifted to a higher-dimensional Fourier feature embedding so the network can capture high-frequency time dependence:

TimeEmb(t) = √(2/d) · [ cos(2πw1t), …, cos(2πwd/2t), sin(2πw1t), …, sin(2πwd/2t) ]

with geometrically spaced frequencies wi from wmin to wmax. Because sin² + cos² = 1, the embedding has unit norm. The exact form is not sacred — it is just a convenient normed embedding of the scalar time.

Class labels. For a discrete label in {0,…,N}, learn one embedding vector per class and look it up — these embeddings are part of θ.

Text. For a text prompt, use a frozen, pre-trained encoder. CLIP (Contrastive Language-Image Pre-training) learns a shared image–text embedding space and gives a single coarse vector y = CLIP(yraw). To preserve word-level detail one also uses a pre-trained transformer (e.g. T5) to get a sequence of embeddings PromptEmbed(yraw) of shape S × k. Models often combine several pretrained text encoders.

The U-Net

The U-Net is a convolutional network whose input and output both have image shape — ideal for parameterizing x ↦ uθt(x|y). It funnels an image down through encoders (channels grow, height/width shrink) to a small latent, processes it in a bottleneck, then expands back through decoders to image shape:

U-Net data flow (example, image 3×256×256).
input 3×256×256 → encoders → latent 512×32×32 → bottleneck → decoders → output 3×256×256.
Residual (skip) connections link matching encoder and decoder levels so fine detail is not lost in the bottleneck. The "U" shape (down then up, with cross-links) gives the architecture its name.

U-Nets dominated the early diffusion literature; modern variants add attention layers inside the encoder/decoder stacks.

The Diffusion Transformer (DiT)

The diffusion transformer applies the attention mechanism instead of convolution. Building on Vision Transformers, it patchifies the image, embeds each patch as a token, processes the token sequence with transformer layers, then depatchifies back to image shape:

DiT data flow. Patchify(x) reshapes C×H×W into N patches (with N = (H/P)(W/P) for patch size P); a linear map gives patch tokens 0 of shape N×d (hidden dim d). Time and prompt are embedded to the same dimension. L DiT blocks update the tokens: i+1 = DiTBlock(x̃i, t̃, ỹ). A final linear map and depatchify return uθt(x|y) in C×H×W.

Inside a DiT block. Three operations: (i) self-attention over patches (each patch attends to all others, the same scaled dot-product attention softmax(QKT/√dh)V with multiple heads); (ii) cross-attention to the prompt tokens (queries from patches, keys/values from y); and (iii) time conditioning via adaptive normalization (AdaLN), where the time embedding produces per-channel scale and shift parameters (γ, β) = g(t̃) that modulate normalized activations, AdaNorm(x) = (1 + γ) ⊙ Norm(x) + β. Class-conditioned DiTs (as in the course lab) usually drop cross-attention in favor of time-and-class AdaLN.

Why DiTs won at scale. Self-attention encodes no domain assumptions — it computes pairwise relationships and aggregates them. It scales smoothly with compute and data, and patch tokens slot naturally beside text tokens for cross-attention. This is exactly the architecture behind Stable Diffusion 3 and Movie Gen, which we examine next.
In a diffusion transformer, how is the time t typically injected into a DiT block, and what shape must the network's output have?

Chapter 11: Latent Space, VAEs, and Real Systems

Modeling images directly in pixel space is brutal: a 1024×1024 RGB image is d ≈ 3 million dimensions, and a flow model's output must be just as large as its input. The fix is to model in a compressed latent space.

Autoencoders and the problem they leave open

An autoencoder pairs an encoder μφ : Rd → Rk with a decoder μθ : Rk → Rd, with k « d (e.g. downsample 1024 to a 16×16-ish latent). Trained on the reconstruction loss E[‖μθφ(x)) − x‖2], it learns to compress and rebuild. But there is no control over the latent distribution platent(z) — compression may turn an easy data distribution into a wild latent one that is hard to model generatively.

The variational autoencoder

A VAE makes the encoder/decoder probabilistic — qφ(z|x) = N(μφ(x), σφ2(x)), pθ(x|z) = N(μθ(z), σθ2) — and adds a regularizer that pushes the latent distribution toward a clean Gaussian prior pprior = N(0, Ik). The objective combines reconstruction with a prior term weighted by β:

LVAE(φ,θ) = −E[ log pθ(x|z) ] + β · E[ DKL(qφ(·|x) ‖ pprior) ]

The first term forces latents to decode back to data; the second forces the per-example encoding to look Gaussian, so the aggregate latent distribution is Gaussian-like and easy to model. The KL divergence DKL(q ‖ p) = Eq[log(q/p)] measures dissimilarity between distributions; it is ≥ 0 and zero iff q = p. For isotropic Gaussians it has a closed form penalizing the encoder mean for being non-zero and the variance for being non-one.

The reparameterization trick. To backprop through the sampling z ∼ qφ(·|x), write z = μφ(x) + σφ(x)·ε with ε ∼ N(0,I). Now the only randomness is ε, whose distribution does not depend on φ — so gradients flow through μφ, σφ normally. (The same trick appears in flow matching, where x = αtz + βtε.) Practical notes: keep β « 1 (large β risks posterior collapse), often warm up β from 0, fix the decoder variance for stability, and add perceptual/adversarial losses for sharper images.

Latent diffusion

The latent diffusion paradigm. First train a (V)AE. Then train the flow/diffusion model entirely in the latent space: encode data to latents at training time, run flow matching there, and at inference sample a latent then decode with x = μθ(z) (use the decoder mean, not a noisy sample, to avoid artifacts). The autoencoder filters out perceptually irrelevant high-frequency detail so the generative model focuses on semantics — and the dimension drop makes ultra-high-resolution generation feasible. Nearly all state-of-the-art image/video models follow this paradigm.

Case studies: Stable Diffusion 3 and Movie Gen

Stable Diffusion 3. Uses exactly the conditional flow matching objective from Chapter 6 (the authors tested many alternatives and found flow matching best), with classifier-free guidance training, inside the latent space of a pretrained autoencoder. Text conditioning combines three embeddings — two CLIP encoders plus the sequential output of Google's T5-XXL — fed to a multi-modal DiT (MM-DiT) that attends to both image patches and text tokens. The largest model has 8 billion parameters; sampling uses 50 Euler steps with a CFG weight between 2.0 and 5.0.

Meta Movie Gen Video. Data are videos in RT×C×H×W. It uses conditional flow matching with the straight-line schedulers αt = t, βt = 1 − t, in the latent space of a temporal autoencoder (TAE) that compresses time and space (with temporal tiling for long videos). The backbone is a DiT patchified over time and space, with self-attention among patches and cross-attention to text. It uses three text encoders (UL2 for reasoning, ByT5 for character-level detail, MetaCLIP for shared image–text space). The largest model has 30 billion parameters.

Why do Stable Diffusion 3 and Movie Gen run the flow matching model in a VAE's latent space rather than in pixel space?

Chapter 12: Discrete Diffusion — Language Models the Diffusion Way

Text is not a point in Rd — it is a sequence of discrete tokens. There is no continuous diffusion process on a discrete set (SDEs need a real-valued state). The principles transfer anyway, with one substitution: replace ODEs/SDEs with continuous-time Markov chains (CTMCs).

The discrete state space

Let the vocabulary be V = {v1,…,vV} (an alphabet, a token set, or the 4 DNA bases). The state space is S = Vd — all sequences of length d. A discrete generative process is a random trajectory Xt in S that is Markov: the future depends only on the present, p(Xt+h | Xt, past) = p(Xt+h | Xt). Such a process is a continuous-time Markov chain.

The rate matrix — the discrete vector field

In discrete space you cannot move "in a direction"; you can only jump between states. The analogue of a vector field is a rate matrix Qt(y|x) — the instantaneous rate of jumping from state x to state y. It obeys two conditions:

Rate matrix conditions. (1) Off-diagonal rates are non-negative: Qt(y|x) ≥ 0 for y ≠ x (you can't have a negative jump rate). (2) Rows sum to zero: Qt(x|x) = −∑y≠x Qt(y|x) (the rate of staying cancels the total rate of leaving — you either stay or go). The "differential equation" condition is d⁄dh pt+h|t(y|x)|h=0 = Qt(y|x).

Theorem 33 (existence & uniqueness): every bounded, time-continuous rate matrix corresponds to a unique CTMC — so we may parameterize Qθt with a network and trust a chain exists. We simulate a CTMC with a discrete Euler step: for small h, pt+h|t(y|x) ≈ 1y=x + h·Qt(y|x) — a valid categorical distribution we sample from each step.

The exponential blow-up and factorization. The state space has |S| = Vd elements — a column of the rate matrix could never fit in memory. Almost all CTMC models are factorized: jumps are only allowed that change one token at a time. The "neighbors" of x are sequences differing in at most one position, and the network outputs a per-position, per-vocabulary rate of shape d × V — which grows linearly, not exponentially, in sequence length. Sampling updates all positions in parallel with a per-token Euler step.

Training mirrors flow matching exactly

The recipe is identical to continuous flow matching: (1) build a probability path interpolating noise and data; (2) derive a conditional and marginal rate matrix; (3) learn the marginal rate matrix simulation-free.

A discrete conditional probability path pt(x|z) again satisfies p0(·|z) = pinit, p1(·|z) = δz, and the marginal path is the data-weighted mixture pt(x) = ∑z pt(x|z) pdata(z). The standard choice is the factorized mixture path: independently per token, with probability κt keep the clean token zj and with probability 1 − κt replace it with a noise token, where the scheduler runs κ0 = 0 to κ1 = 1. Unlike the Gaussian path it does not transport mass — it fades one distribution out and another in (there is no direction in discrete space).

Theorem 36 / 38 (Discrete marginalization). Just like the continuous case, the marginal rate matrix is a posterior-weighted combination of conditional rate matrices, and (proved via the Kolmogorov Forward Equation, the discrete analogue of the continuity equation) its CTMC follows the marginal path, so X1 ∼ pdata. For the factorized mixture path the marginal rate matrix reduces to a reparameterization of p1|t(zj = vi | x) — the probability that clean token j equals vi given the noisy sequence x. That is just a classifier per token position.

So training reduces to cross-entropy. The discrete flow matching loss is the token-wise negative log-likelihood of the clean tokens given the noised sequence:

LDFM(θ) = Ez∼pdata, t, x∼pt(·|z) [ − ∑j=1..d log pθ1|t(zj | x) ]

The parallel to continuous flow matching is exact: there, training reduced to regression against a velocity; here, it reduces to classification of the clean tokens. A standard sequence-to-sequence transformer with a softmax per position is enough.

Masked diffusion language models

Example 39 — Masked Diffusion LMs (MDLMs). Add a special [MASK] token to the vocabulary and set the initial distribution to the all-masked sequence [MASK]d. The factorized mixture path then progressively unmasks tokens. Generation starts from a fully masked sequence and reveals tokens over time — e.g. [MASK][MASK][MASK][MASK] → "[MASK] cat [MASK] on" → "The cat sat on the mat." At each step the network (a transformer) predicts the clean token at each still-masked position; the CTMC fills them in. This is how diffusion-style language models are built from exactly the principles of Chapters 4–6 — only the geometry (discrete jumps instead of continuous transport) differs.
In discrete diffusion with the factorized mixture path, what does training a CTMC model ultimately reduce to?

Chapter 13: Cheat Sheet & Connections

You now hold the whole document — from "generation is sampling" to masked diffusion language models. Here it is on one page.

The spine. Generation = sampling pdata (seen only through examples). Transport an easy pinit to pdata by simulating a process. Flow = ODE; diffusion = SDE (ODE + Brownian noise); ODE is the σt = 0 case. Train the generator simulation-free by regressing the network against a tractable conditional target whose loss shares a gradient with the intractable marginal one. Guidance steers toward a prompt. Discrete data swaps the ODE/SDE for a CTMC and regression for classification.

Cheat sheet

ODE / field / flow. d⁄dt Xt = ut(Xt); flow ψt(x0) solves it for all starts; linear field u(x)=−θx ⇒ ψt=exp(−θt)x0. Euler: Xt+h=Xt+h·ut(Xt), first-order.

SDE / Euler–Maruyama. dXt=ut(Xt)dt+σtdWt; step Xt+h=Xt+h·utt√h·ε. Brownian: Var[Wt]=t, kick √h·ε. OU: dXt=−θXtdt+σdWt ⇒ N(0,σ²/2θ).

Probability paths. Conditional pt(x|z): noise → δz. Marginal pt(x)=∫pt(x|z)pdata(z)dz: samplable, density intractable. Gaussian: N(αtz,βt2I), sample x=αtz+βtε.

Marginalization trick. utargett(x)=∫utargett(x|z)·posterior(z|x)dz follows the marginal path (continuity equation). Gaussian conditional field (α̇t−(β̇ttt)z+(β̇tt)x.

Flow matching loss. LCFM=E‖uθt(x)−utargett(x|z)‖2; same gradient as LFM (Thm 12). Simulation-free, just regression. CondOT target = z−ε.

Score. ∇ log pt; Gaussian conditional −(x−αtz)/βt2=−ε/βt. Field ↔ score (Prop 1). DDPM: predict ε.

SDE extension. dXt=[utargett+(σt2/2)∇ log pt]dt+σtdWt keeps marginals (Fokker–Planck). σt=0 ⇒ probability-flow ODE. Langevin = static special case.

Guidance. CFG: ũt(x|y)=(1−w)utargett(x|∅)+w·utargett(x|y), one model via null-label dropout, w≥4 in practice.

Architectures. Inputs x,y,t → output velocity (same shape as x). Fourier time embedding, U-Net (conv, encoder/decoder + skips), DiT (patch tokens + attention + AdaLN). Latent diffusion in a VAE space (SD3, Movie Gen).

Discrete. CTMC with rate matrix Qt(y|x) (factorized, d×V). Marginal rate = reparam of p1|t; train by per-position cross-entropy. MDLM = unmask from [MASK]d.

The five lectures (Gleams) this companion accompanies

Related lessons on this site

The Feynman test. Open the lecture notes. Can you now, from memory, explain generation-as-sampling, derive the linear flow, motivate the √h in Brownian motion, state the marginalization trick and why the conditional loss shares the marginal gradient, convert between field and score, justify the SDE extension via Fokker–Planck, derive classifier-free guidance, sketch a DiT block, and explain how masked diffusion LMs reduce to per-token classification? If yes — you own these notes.
One sentence: what single principle unifies flow matching, score matching, and discrete diffusion training?