Problem
A generative model should do two things: draw new samples, and say how likely a given sample is. VAEs give a bound on the likelihood, not the likelihood itself; diffusion models sample well but need hundreds of network calls, and their likelihood is a bound unless you solve an ODE.
A normalizing flow makes the likelihood exact by construction. Pick a base distribution you can sample and score, usually a standard normal, and an invertible, differentiable map f. Sample by pushing noise through f; score a data point by pulling it back through f⁻¹ and correcting for how much f stretched space. The hard part is building maps that are expressive, invertible and have a cheap Jacobian determinant. Continuous flows sidestep the architecture constraints by defining f as the solution of an ODE, and flow matching is the training objective that made them practical at scale.
Intuition
Picture probability as sand. The base distribution is a smooth pile; the data distribution is the shape you want. A flow is a set of instructions for moving every grain, and because grains are neither created nor destroyed, wherever the map squeezes space the density goes up by exactly the factor it squeezed. That bookkeeping is the change-of-variables formula.
A continuous flow gives each grain a velocity at every moment instead of one big jump. Flow matching learns that velocity field by regression: take a noise grain and a data grain, draw the straight line between them, and teach the network that a grain at a point on the line at time t should be moving along it. Many lines pass through the same point, so the network learns their average direction, and following those averaged directions turns out to carry the whole noise pile onto the whole data pile.
Sampling runs left to right; scoring runs right to left, inverting each transform and subtracting its log-Jacobian. This is exactly what TransformedDistribution.log_prob does.
Mechanics
Change of variables
If z has density pZ and y = f(z) with f invertible and differentiable, then
log pY(y) = log pZ(f⁻¹(y)) − log |det ∂f/∂z|, evaluated at z = f⁻¹(y)
The determinant measures how much f expands a small volume around z; expanding spreads the same probability over more space, so the density drops. A composition f = fK ∘ … ∘ f1 adds one log-determinant per step, which is why flows are built from many simple layers. PyTorch implements this directly: a Transform provides the forward map, its inverse and log_abs_det_jacobian(x, y), and TransformedDistribution.log_prob walks the transforms in reverse, inverting each and subtracting its log-Jacobian before scoring the base. LogNormal is literally TransformedDistribution(Normal(loc, scale), ExpTransform()).
For a general D-dimensional map, the determinant costs O(D³). Normalizing-flow research is largely the search for layers whose Jacobians are triangular, so the determinant is the product of the diagonal.
Coupling layers
A coupling layer splits the input into halves xa and xb, leaves xa alone and transforms xb with parameters computed from xa. The affine coupling of RealNVP is
ya = xa, yb = xb ⊙ exp(s(xa)) + t(xa), log |det J| = Σ s(xa)
The Jacobian is block triangular with an identity block and a diagonal block exp(s), so its log-determinant is just the sum of s. Inverting needs no inverse of the networks s and t: xb = (yb − t(ya)) ⊙ exp(−s(ya)), because ya = xa. s and t can be any networks. Stacking layers that alternate which half is left alone lets every dimension be transformed. NICE used the additive special case (s = 0), which preserves volume; Glow added learned invertible 1×1 convolutions to mix channels between couplings. Autoregressive flows generalize the idea with one conditioner per dimension.
Continuous flows
Make the layers infinitely thin. A continuous normalizing flow defines the map as the solution of an ODE with a learned velocity field vθ:
dx/dt = vθ(x, t), x(0) ~ p₀ (noise), x(1) = the sample
Any reasonable vθ gives an invertible map (run the ODE backwards), with no architectural constraint. The log density changes along a trajectory by the instantaneous change of variables from the Neural ODE paper:
d log p(x(t)) / dt = − tr(∂vθ/∂x) = − div vθ
Computing the trace exactly costs about D backward passes, so FFJORD estimated it with Hutchinson's trick, εᵀ (∂v/∂x) ε for random ε. Training these models by maximum likelihood still meant solving the ODE inside every training step, which was slow. Flow matching removes that.
The flow-matching objective
Choose a probability path pt from noise p₀ to data p₁, and regress vθ onto the velocity ut that generates it. That velocity is unknown, but it is easy for one data point at a time. With the straight-line path
xt = (1 − t) x₀ + t x₁, x₀ ~ N(0, I), x₁ ~ data, t ~ U[0, 1]
each pair moves at constant velocity x₁ − x₀. The conditional flow-matching loss regresses onto that per-pair target:
LCFM(θ) = Et, x₀, x₁ ‖ vθ(xt, t) − (x₁ − x₀) ‖²
The minimizer of a squared loss is a conditional mean, so the best vθ is v*(x, t) = E[x₁ − x₀ | xt = x], the average direction of all lines through x at time t. Lipman et al. show that this marginal velocity generates the marginal path pt, and that the conditional loss has the same gradients as regressing on the intractable marginal velocity directly. The loss never reaches zero: what is left is the variance of x₁ − x₀ among the lines through each point. Lipman et al. write the path with a small floor, xt = (1 − (1 − σmin) t) x₀ + t x₁, calling it the optimal-transport conditional path; σmin = 0 gives the line above. Training needs no ODE solve, no simulation and no noise schedule: one network call and one squared error per example, like a denoising loss.
Rectified flow and reflow
Rectified flow (Liu et al.) uses the same straight-line interpolation and loss. Its focus is what happens to the paths. Because the pairs (x₀, x₁) are drawn independently, their lines cross, and at a crossing the learned velocity averages the two directions. So the ODE trajectories of the trained model do not cross (an ODE's solutions never do) and are curved, and a coarse solver following them has to take many steps. Reflow fixes this: sample noise x₀, run the trained ODE to get its endpoint T(x₀), and train a new flow on the pairs (x₀, T(x₀)). Those pairs already come from a flow whose paths do not cross, so the new lines cross less, and the new model's paths are straighter. A perfectly straight flow is solved exactly by a single Euler step, which is why reflow, followed by distillation, gives few-step and one-step samplers. Stable Diffusion 3 trains its image model with the rectified-flow objective.
Relation to diffusion
Diffusion corrupts data with xt = √ᾱ x₀ + √(1 − ᾱ) ε, with data at the start of time; flow matching writes xt = t x₁ + (1 − t) x₀ with data at the end. Both are Gaussian paths x = α · data + σ · noise, and only α and σ differ: dividing the flow's xt by √(t² + (1 − t)²) gives a diffusion point with ᾱ = t² / (t² + (1 − t)²). The optimal predictions are linked too. With x̂₁ = E[x₁ | xt] the predicted data and x̂₀ = E[x₀ | xt] the predicted noise,
v*(xt, t) = x̂₁ − x̂₀ = (x̂₁ − xt) / (1 − t)
so a velocity network, a denoiser and a noise predictor are three parametrizations of one function. Every diffusion model also has a deterministic probability-flow ODE (Song et al.) with the same marginals as its stochastic sampler, and DDIM's update is a first-order solver for it written in other variables. Flow matching picks the straight path and the velocity target directly, which in practice gives straighter trajectories and a simpler training recipe.
Sampling with an ODE solver
Sampling integrates dx/dt = vθ(x, t) from t = 0 to 1. Euler takes N steps of x ← x + h · v(x, t) with h = 1/N, one network call each. Heun (a second-order Runge-Kutta method) takes an Euler step, evaluates the velocity at its end, and moves by the average of the two slopes, two calls per step and error falling as h² instead of h. Adaptive solvers such as Dormand-Prince pick their own step sizes from an error estimate. PyTorch has no ODE solver in the core library; the loop is a few lines, as in the implementation below, and libraries such as torchdiffeq provide adaptive ones. Straighter paths need fewer steps, which is the practical case for the straight-line path and for reflow. Likelihoods need the trace integral above solved alongside x, which is why flow-matching models are mostly used for sampling.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
Transform |
__call__, inv, log_abs_det_jacobian(x, y) |
The base class for an invertible map: forward, inverse (via _inverse), and the elementwise log |dy/dx| that the change of variables subtracts. |
transforms.py 47-211 |
ComposeTransform |
list of transforms | Chains transforms; its log-Jacobian is the sum of the parts', each evaluated at its own input. | transforms.py 286-411 |
AffineTransform(loc, scale) |
y = loc + scale · x; log-Jacobian log |scale| | The affine step of the diagram, and the building block of an affine coupling when loc and scale come from a network. | transforms.py 752-856 |
ExpTransform |
y = exp(x); log-Jacobian x | Maps the real line to positive values; its log_abs_det_jacobian returns x itself. |
transforms.py 571-591 |
TransformedDistribution |
base distribution, list of transforms | rsample pushes base samples forward; log_prob inverts the transforms in reverse order, subtracting each log-Jacobian, then adds the base log density. |
transformed_distribution.py 16-226 (log_prob 168-190) |
LogNormal |
loc, scale | A one-line flow: Normal(loc, scale) through ExpTransform(). |
log_normal.py 13-75 |
Velocity network nn.Sequential of nn.Linear, nn.SiLU |
input (B, D + 1) with t appended; output (B, D) | Any network that maps (x, t) to a vector of the same shape as x; images use a U-Net or a transformer with a time embedding instead. | container.py 59-338, linear.py 53-147, activation.py 436-483 |
The training target x₁ − x₀ is different for every pair, but the learned field is a single function of (x, t). What does the network converge to?
The conditional mean E[x₁ − x₀ | xt = x] at each point and time: the average direction of all the lines that pass through x at time t, weighted by how likely each pair is to be there. That average is the marginal velocity, and following it moves the whole noise distribution onto the whole data distribution, even though no single sample follows its own training line.
Worked example
Scoring a point through two transforms
Setup. The flow of the first diagram: z ~ N(0, 1), x = 0.2 + 0.5 z, y = exp(x), which is LogNormal(0.2, 0.5). Score y = 1.5.
Invert. x = log 1.5 = 0.405; z = (0.405 − 0.2) / 0.5 = 0.411. Score. log N(z) = −z²/2 − ½ log 2π = −1.003. The affine step subtracts log 0.5, adding 0.693; the exp step subtracts x, adding −0.405. Total −0.716, which matches the closed-form log-normal density.
Check. Integrating exp(log p(y)) by the trapezoid rule over (0, 40] with 4000 intervals gives 1.000000. Leave out the −x term and the "density" would no longer integrate to 1: the Jacobian term is what keeps probability conserved.
One affine coupling layer
Setup. x = (0.5, −1.2), with s(a) = 0.8 tanh(a) and t(a) = 0.5 a − 0.3. Forward. s = 0.370, t = −0.050, so y = (0.500, −1.2 · exp(0.370) + (−0.050)) = (0.500, −1.787). Log-determinant. s = 0.370, so the layer stretches area by exp(0.370) = 1.4473.
Check. Central finite differences give the Jacobian [[1.0000, 0.0000], [−0.5927, 1.4473]]: lower triangular, as promised, with determinant 1.4473. The off-diagonal entry, how yb depends on xa, is not zero and does not matter for the determinant. Running the inverse on y returns (0.500, −1.200).
A continuous flow with a known answer
Setup. Noise x₀ ~ N(0, 1), data x₁ ~ N(2.0, 0.5²), independent pairs and the straight-line path. Then xt is Gaussian with mean 2.0t and variance σt² = (1 − t)² + 0.5² t², and the marginal velocity, the regression of x₁ − x₀ on xt, is
v*(x, t) = 2.0 + (0.5² t − (1 − t)) / σt² · (x − 2.0t), div v* = (0.5² t − (1 − t)) / σt²
Start at z = 1.0. In one dimension the flow must map each quantile to the same quantile, so the exact endpoint is 2.0 + 0.5 · 1.0 = 2.500 with log density −0.726. Euler integration of x and of log p (subtracting h · div v* each step):
| Euler steps | x(1) | log p tracked along the path | True log density at that x(1) |
|---|---|---|---|
| 1 | 2.000 | −0.419 | −0.226 |
| 2 | 2.200 | −0.319 | −0.306 |
| 4 | 2.341 | −0.495 | −0.458 |
| 8 | 2.415 | −0.606 | −0.570 |
| 64 | 2.489 | −0.710 | −0.703 |
| exact | 2.500 | −0.726 | −0.726 |
One step evaluates the velocity at t = 0, where it is 2.0 − x, so every start lands exactly on the mean 2.000: the spread of the data is lost. More steps converge, with error roughly halving as steps double, as expected of a first-order method. The tracked log density converges to the exact −0.726, which is the instantaneous change of variables at work: squeezing by a factor of 0.5 adds −log 0.5 = 0.693 to the base log density −1.419.
The learned velocity for two-point data
Setup. The data are ±1 with equal probability, as on the diffusion page, and x₀ ~ N(0, 1). Given xt = x, the posterior over x₁ = ±1 comes from two Gaussians of width 1 − t centred at ±t, so
x̂₁ = E[x₁ | xt = x] = tanh(t x / (1 − t)²), x̂₀ = (x − t x̂₁) / (1 − t), v* = x̂₁ − x̂₀
At x = 0.5, t = 0.5: x̂₁ = tanh(1) = 0.7616, x̂₀ = 0.2384, v* = 0.5232. The irreducible part of the loss there, Var(x₁ − x₀ | xt) = (1 − x̂₁²) / (1 − t)², is 1.680: even a perfect network has this much loss at this point. A Monte Carlo check in the tests keeps the pairs whose xt lands near 0.5 and recovers both numbers.
The diffusion view. Dividing x by √(t² + (1 − t)²) gives the diffusion point x = 0.7071 at ᾱ = 0.500, and the diffusion page's optimal denoiser there returns 0.7616, the same x̂₁. Same function, different coordinates.
Euler and Heun on the two-point flow
Setup. 400 starting points at the quantiles of N(0, 1), integrated with the exact v* above. A good sampler sends half to each of ±1. Heun uses Euler for its final step so that it never evaluates v at t = 1; it costs 2N − 1 velocity evaluations.
| Steps N | Euler: fraction > 0 | Euler: mean |x| | Euler: max ||x| − 1| | Heun: mean |x| | Heun: max ||x| − 1| | Heun evaluations |
|---|---|---|---|---|---|---|
| 1 | 0.00 | 0.000 | 1.000 | 0.000 | 1.000 | 1 |
| 2 | 0.50 | 0.556 | 0.997 | 0.730 | 0.994 | 3 |
| 4 | 0.50 | 0.950 | 0.966 | 0.995 | 0.691 | 7 |
| 8 | 0.50 | 1.000 | 0.028 | 1.000 | 0.000 | 15 |
| 16 | 0.50 | 1.000 | 0.000 | 1.000 | 0.000 | 31 |
| 64 | 0.50 | 1.000 | 0.000 | 1.000 | 0.000 | 127 |
| 256 | 0.50 | 1.000 | 0.000 | 1.000 | 0.000 | 511 |
One Euler step evaluates v* at t = 0, where x̂₁ = 0 and v* = −x, so every sample lands on 0, the mean of the data, just like the Gaussian case. From two steps on, the signs are right; by 16 Euler steps every sample sits on ±1 to printed precision. That last part is special to this data: once x̂₁ has saturated at ±1, the last Euler step x + (1 − t) · (x̂₁ − x)/(1 − t) lands exactly on x̂₁, as DDIM's last step returns its x̂₀. Per step, Heun is ahead: at 4 steps its mean |x| is 0.995 against 0.950. Per velocity evaluation it is not, on this data: 7 Heun evaluations leave a worst error of 0.691, while 8 Euler steps leave 0.028. The velocity turns sharply late in time, when x̂₁ snaps to ±1, and a second-order method only pays off when the field is smooth across a step, one reason practical samplers space their steps unevenly in time.
Crossing lines and curved paths
Crossing. The training pairs (−1.0 → 1.0) and (0.5 → −1.0) meet at t = 0.429 (3/7), x = −0.143. A network trained on both sees opposite targets at that point and learns their average, so its trajectories bend instead.
Curvature. Following v* with 1000 Euler steps, the path from 0.1 has displacement 0.900 but length 0.950: it first drifts toward 0, then turns and heads for +1. The straightness defect, the time average of (x(1) − x(0) − dx/dt)², which is zero exactly for a constant-speed line, averages 0.587 over every eighth of the 400 quantile starting points.
Reflow. Pair each start x₀ with its endpoint T(x₀) and use the lines between them. The endpoints are in the same order as the starts, so for t < 1 no two lines meet, and the velocity at each point is just its line's slope. Euler with 1, 2 and 4 steps on that field reproduces every endpoint, worst errors 0, 2e-16, 1e-16, rounding only. One network call per sample, where the original flow gave 0 for every sample with one step.
Left: two straight training lines from independent pairs cross, so the regression target at the red circle is ambiguous. Right: the flow that the regression actually learns. The amber path from 0.1 shows the detour; reflow would replace each path with the straight line from its start to its end.
Implementation
import torch
from torch import nn
from torch.distributions import Normal, TransformedDistribution
from torch.distributions.transforms import AffineTransform, ExpTransform
# A discrete flow: change of variables through two transforms.
flow = TransformedDistribution(Normal(0.0, 1.0), [AffineTransform(0.2, 0.5), ExpTransform()])
flow.log_prob(torch.tensor(1.5)) # −0.7157, the same as LogNormal(0.2, 0.5)
# A continuous flow trained with flow matching.
D = 2
net = nn.Sequential(nn.Linear(D + 1, 256), nn.SiLU(),
nn.Linear(256, 256), nn.SiLU(),
nn.Linear(256, D))
opt = torch.optim.Adam(net.parameters(), lr=1e-3)
def velocity(x, t): # x: (B, D), t: (B, 1)
return net(torch.cat([x, t], dim=-1))
def train_step(x1): # x1: a batch of data, (B, D)
x0 = torch.randn_like(x1) # noise
t = torch.rand(x1.shape[0], 1) # one time per example
xt = (1 - t) * x0 + t * x1 # point on the straight line
loss = ((velocity(xt, t) - (x1 - x0)) ** 2).mean()
opt.zero_grad()
loss.backward()
opt.step()
return loss.item()
@torch.no_grad()
def sample(n, steps=32): # Euler from noise (t = 0) to data (t = 1)
x = torch.randn(n, D)
for k in range(steps):
t = torch.full((n, 1), k / steps)
x = x + velocity(x, t) / steps
return x
The training step has no ODE in it: the only randomness is the noise, the time and the batch, and the target is a subtraction. Swapping the Euler loop for a Heun loop, or for an adaptive solver from a library, changes only sample. For reflow, run sample on a fixed batch of noise, keep each (noise, output) pair, and train a fresh net with x0 and x1 taken from those pairs instead of drawn independently.
Tradeoffs
| Approach | Likelihood | Sampling cost | Main constraint |
|---|---|---|---|
| Coupling and autoregressive flows (RealNVP, Glow) | Exact, one pass | One pass | Invertible layers with triangular Jacobians limit expressiveness; latent has the same dimension as the data |
| Continuous flow trained by likelihood (Neural ODE, FFJORD) | Exact up to solver error, needs the trace | An ODE solve | Every training step solves the ODE; slow |
| Flow matching / rectified flow | Available through the ODE and trace, rarely used | An ODE solve; tens of steps, fewer after reflow or distillation | Paths curve where training lines cross |
| Diffusion | A bound, or exact through the probability-flow ODE | Many steps with a stochastic sampler, fewer with ODE solvers | Noise schedule and parametrization choices |
- Straight paths buy cheap sampling. The number of solver steps is set by how curved the paths are, not by the network. Reflow straightens them at the cost of generating a dataset of pairs and training again, and each round adds some error of its own.
- The path is a design choice. The straight line and diffusion's variance-preserving path describe the same family with different α and σ; which works better also depends on how t is sampled during training, for which Stable Diffusion 3 reports gains from weighting the middle of the path.
- Exact likelihood is rarely the point. Discrete flows are used where an exact density matters, and as components of other models; for sample quality, large image models have moved to diffusion and flow matching.
Why does one Euler step of a flow-matching model produce a blurry average instead of a sample?
The first step evaluates the velocity at t = 0, where xt is pure noise and says nothing about which data point it is heading to, so x̂₁ is the data mean and the step x₀ + (x̂₁ − x̂₀) lands on the mean. On two-point data every sample lands on 0. Getting a sharp sample in one step needs paths that are already straight for each start, which is what reflow and distillation provide.
A coupling layer leaves half its input unchanged. How can a stack of them model a distribution where every dimension depends on every other?
By alternating the split. A layer that conditions xb on xa is followed by one that conditions xa on the new xb, so after two layers every output depends on every input. Glow's learned 1×1 convolutions go further, mixing channels between couplings so the split is not fixed in advance.
Connections
Read first
- Diffusion models advanced
Read next
No page builds on this one yet.
Diffusion models are the closest relative: the same Gaussian paths in other coordinates, and DDIM is an ODE sampler like the ones here. VAEs also map between noise and data but approximate the likelihood, and flows have been used inside them as richer approximate posteriors. The training loss is ordinary regression, so everything on optimizers and backpropagation applies unchanged.
References
- Dinh, Krueger and Bengio. NICE: Non-linear Independent Components Estimation. 2014. arXiv:1410.8516
- Dinh, Sohl-Dickstein and Bengio. Density Estimation Using Real NVP. ICLR 2017. arXiv:1605.08803
- Kingma and Dhariwal. Glow: Generative Flow with Invertible 1x1 Convolutions. NeurIPS 2018. arXiv:1807.03039
- Chen, Rubanova, Bettencourt and Duvenaud. Neural Ordinary Differential Equations. NeurIPS 2018. arXiv:1806.07366
- Grathwohl et al. FFJORD: Free-form Continuous Dynamics for Scalable Reversible Generative Models. ICLR 2019. arXiv:1810.01367
- Song et al. Score-Based Generative Modeling through Stochastic Differential Equations. ICLR 2021. arXiv:2011.13456
- Song, Meng and Ermon. Denoising Diffusion Implicit Models. ICLR 2021. arXiv:2010.02502
- Lipman et al. Flow Matching for Generative Modeling. ICLR 2023. arXiv:2210.02747
- Liu, Gong and Liu. Flow Straight and Fast: Learning to Generate and Transfer Data with Rectified Flow. ICLR 2023. arXiv:2209.03003
- Albergo and Vanden-Eijnden. Building Normalizing Flows with Stochastic Interpolants. ICLR 2023. arXiv:2209.15571
- Esser et al. Scaling Rectified Flow Transformers for High-Resolution Image Synthesis. 2024. arXiv:2403.03206
- PyTorch v2.15.0:
Transform,AffineTransform,ExpTransform,TransformedDistribution,LogNormal.