DDPM ↔ Flow matching
They look like two different methods. They are the same object, viewed from two angles.
Both fit a local operator on a density path
Strip away the surface differences:
| DDPM | Flow matching | |
|---|---|---|
| density path | q(x_t | x₀) = N(√ᾱ_t x₀, (1−ᾱ_t) I) VP “curved” path | chosen by you, e.g. (1−t)x₀ + t·x₁ linear (OT) path |
| training target | noise ε that perturbed x₀ → x_t | velocity (x₁ − x₀) along the segment |
| training loss | ‖ε − ε_θ(x_t, t)‖² | ‖v_θ(x_t, t) − (x₁ − x₀)‖² |
| sampler | ancestral Gaussian, 1000 steps | Euler ODE, 50 steps |
| derivation framework | ELBO + Gaussian KL collapse | continuity equation + conditional E[·] |
The cells that look different are surface: path shape, what the net predicts, which solver you use. The cells that matter (training-by-regression-against-a-tractable-conditional-target, sampling-by-iterated-local-update) are identical.
The mapping: ε ↔ v
For the VP path used by DDPM, the conditional velocity along the path is
Here is the whole trick in words before any symbols. A point on the path is built from two ingredients — a shrinking piece of the clean data and a growing piece of the noise. The velocity is just how fast that mixture is changing, so it too is built from those same two ingredients. But we don’t store the clean data x0 at sampling time — we only have the noisy point xt and the network’s noise guess εθ. So we trade x0 away: substitute its DDPM-form value x0 = (xt − √(1−ᾱt) ε) / √ᾱt (Eq. ⋆ inverted), and the velocity comes back out written purely in (xt, ε). The upshot: any DDPM ε-predictor already is a flow-matching velocity along the VP path, and vice versa — you just rewrite it. With the standard shorthand αt := ᾱt and σt := √(1 − ᾱt) (dot = d/dt):
Don’t memorize the algebra; remember the structural claim: v is an affine function of x and ε with time-dependent coefficients fully determined by the schedule. Score, noise, velocity are three names for the same object up to the path’s known scaling.
Claim: a DDPM noise-predictor and a flow-matching velocity-predictor are the same network wearing different labels — converting one to the other is just multiply-and-add with numbers the schedule already fixes.
- One path, two readings. Both methods walk the same curve from data to noise. DDPM reports “which noise ε sits at this point?”; flow matching reports “which direction v is this point moving?” Same curve — so knowing one answer must pin down the other.
- Why it’s only multiply-and-add. The point xt = √ᾱt x0 + √(1−ᾱt) ε is a straight-line mix of data and noise. Velocity is its time-derivative, and the derivative of a straight mix is again a straight mix — no curves enter. So v can only be xt and ε each scaled by some number, then added.
- Where the numbers come from. Those scaling numbers are pure schedule — the coefficients α̇t/(2αt) and the σ̇t term — chosen up front, identical for every datapoint. The network never has to learn the conversion; it’s arithmetic on known constants.
- Hence interchangeable. Predict ε and you can read off v on the spot; train for v and you can read off ε. Neither is more fundamental; the path picks the dictionary between them.
Central point. “Noise” and “velocity” aren’t rival quantities to choose between — they are one quantity in two coordinate systems, and the schedule is the fixed exchange rate.
Score, noise, velocity — the three-way identity
The score of q(xt | x0) — the gradient of its log density — is
st(x | x0) = ∇x log q(x | x0) = −(x − √ᾱt x0) / (1 − ᾱt)
Plug in x = √ᾱt x0 + √(1−ᾱt) ε and the cancellation gives
st(x | x0) = −ε / √(1 − ᾱt)
So noise prediction = scaled score prediction = scaled velocity prediction (along VP path). The same network with three name tags.
The deeper view: SDE limits
Take DDPM’s discrete chain and let the per-step β go to zero with the number of steps T → ∞ at a constant rate. The chain becomes a continuous-time SDE:
(Song et al. 2021, “Score-Based Generative Modeling Through Stochastic Differential Equations”.) For every SDE there’s a corresponding probability-flow ODE — the deterministic ODE whose marginals match the SDE’s. That probability-flow ODE is the flow-matching ODE for the VP path. The mapping above is the one between the SDE’s drift and the ODE’s velocity, made explicit.
So:
- DDPM (discrete) is the discretization of the VP SDE.
- The VP probability-flow ODE is flow matching with the VP path.
- FM with the linear path is the same machinery on a different path — one that’s straighter at sampling time.
Claim: “DDPM is a special case of flow matching” means DDPM is one particular path, sampled with extra randomness, where flow matching lets you pick the path.
- Make the chain continuous. DDPM’s 1000 discrete denoising steps are a staircase. Shrink each step toward zero and the staircase becomes a smooth ramp — an SDE. Nothing new is added; we just stop pretending time is chunky.
- Split off the dice-rolling. That smooth process has two parts: a deterministic drift (where the cloud of samples is heading on average) and a random jitter (the coin-flip each step adds). The drift alone carries all the information about where probability mass goes.
- Keep the drift, drop the dice. There is a purely deterministic ODE — the probability-flow ODE — that moves each point so the overall distribution at every time matches the noisy process exactly. Same destinations, no jitter. That deterministic ODE is a flow-matching velocity field, just for the curved VP path.
- So FM is the bigger room. DDPM = this one curved path, run with the jitter still on. Flow matching = the freedom to choose any path (a straight one runs the same machinery with fewer, cleaner solver steps). DDPM is the special case; FM is the general setting.
Central point. The SDE story isn’t new math — it’s the same DDPM process, made smooth and stripped of its randomness, which reveals it was a flow-matching velocity field on a particular path all along.
This is the “DDPM is a special case of flow matching” line from the README. It’s correct, but more useful as a perspective than as a derivation aid: the SDE language is heavier than either DDPM or FM alone.
Interactive · two paths, same destination
The widget below schematically contrasts two routes from a fixed x0 to a fixed x1: the straight FM linear path and a curved VP-flavored route. Strictly speaking, the VP forward marginal mean 𝔼[xt | x0] = √ᾱt · x0 shrinks monotonically toward the origin (it does not swing out to a fixed x1); the orange curve here is a Bézier schematic of “shrink toward zero, then bloom out” that captures the qualitative curvature of the sampling-time trajectory. The takeaway is just: curved routes need more integrator steps.
Interactive · the two trajectory bundles, side by side
Below: two 3D plots of the same particles’ trajectories under the two methods, drawn at matched compute. Left is DDPM ancestral (~T = 200 steps, curved + jittery); right is FM Euler (~K = 30 steps, straight + clean). Same endpoints (same final mode assignments), different routes. The picture is the argument for FM’s sampling cost.
Practical consequence: pick by sampling-step budget
| Budget (forward passes) | Pick | Why |
|---|---|---|
| 1000+ | DDPM ancestral | Reference quality; you have the budget for the curved path |
| 50–100 | DDIM or FM-Euler | Either works; FM is one design choice cleaner |
| 10–20 | FM-RK4 or DPM-Solver++ | Higher-order solver on a straight path |
| 1–4 | Distilled student or consistency model | Below this, you’re fundamentally re-training the model to be a one-shot sampler |
What the code shows
In this repo, the same DiT class (diffusion_transformer.py) is used as both the ε-predictor for DDPM (make_ddpm_dit) and the v-predictor for flow matching (make_flow_dit). The architecture doesn’t know which one it is. The two factories differ in one line:
# diffusion_transformer.py (make_ddpm_dit, line ~289)
denoiser = DiT(..., t_scale=1.0)
# flow_matching_transformer.py (make_flow_dit, line ~56)
velocity = DiT(..., t_scale=1000.0)
That’s the difference. Everything else — patch embed, attention, adaLN-Zero, unpatch, optimizer — is shared. Whether the same parameters mean “predict noise” or “predict velocity” is decided entirely by which loss you train under. We’ll dig into the architecture next lesson.