Flow Matching and Consistency Models
DDPM Sampling and Guidance got diffusion sampling down to tens of steps. This page covers the research direction aimed squarely at pushing that further — toward single-digit, and eventually single-step, generation, by rethinking what the network is trained to predict in the first place.
Instead of learning to denoise, learn the velocity field that carries noise to data along a straight path, and the path becomes cheap to follow.
The step-count problem restated
Every diffusion sampler, however clever, is still numerically integrating a trajectory from noise to data — and that trajectory, as parameterised by standard DDPM training, tends to be curved, which is precisely why integrating it accurately needs many small steps. If the trajectory were straighter, fewer, larger steps could integrate it just as accurately.
Continuous-time diffusion as an ODE/SDE
Reframe the discrete-timestep diffusion process as the discretisation of a continuous-time stochastic differential equation (SDE) — the forward noising process and reverse denoising process become, respectively, a forward and (time-reversed) SDE over . This continuous view is what makes the following reformulations possible.
The probability flow ODE
Remarkably, there exists a deterministic ordinary differential equation (ODE) — the probability flow ODE — whose trajectories have exactly the same marginal distributions at every timestep as the stochastic reverse SDE. Because it's deterministic, integrating it is a standard numerical ODE problem, and DDIM (from the previous page) is, in fact, one particular numerical solver for exactly this ODE.
Flow matching: regress a velocity field directly
Rather than training a network to predict noise and only implicitly defining a trajectory through the diffusion mathematics, flow matching directly regresses a velocity field that describes how a point should move at time to travel from the noise distribution to the data distribution:
where is a simple interpolation between a noise sample and a data sample . This sidesteps the diffusion SDE/ODE derivation entirely — the target velocity is just "the straight-line direction from here to the data point," directly supervised.
Rectified flow, and why straighter paths need fewer steps
Rectified flow trains on straight-line interpolations between paired noise and data samples by construction, then iteratively "reflows" — retraining on straightened trajectories generated by the current model — pushing the learned paths ever closer to genuinely straight lines. A perfectly straight path can, in principle, be integrated exactly in a single step; the closer training gets the paths to straight, the fewer integration steps are needed for a given quality level.
The relationship to diffusion
Flow matching and diffusion are not competing model families so much as different training parameterisations converging on the same underlying idea — both learn to map noise to data along a continuous trajectory; flow matching just supervises the velocity directly instead of deriving it implicitly through a noise-prediction objective. Many practical systems now use flow-matching-style training with diffusion-style architectures.
Consistency models: map any point on the trajectory straight to the endpoint
A consistency model trains a network to map any point along a trajectory directly to that trajectory's data endpoint, in a single function evaluation — rather than only predicting the next small step. If the model succeeds, sampling requires no iterative integration at all: one forward pass from any noise level straight to a sample.
Distillation of a many-step teacher into a few-step student
The most common way to train a consistency model in practice: start from an already-trained multi-step diffusion (or flow-matching) model as a teacher, and train the consistency model as a student to match the teacher's full multi-step trajectory using far fewer evaluations — the general knowledge-distillation idea, applied to trajectories rather than output labels.
The current quality/speed frontier, described honestly as a moving target
As of this writing, few-step and single-step generative models close most, but not all, of the quality gap to their many-step teachers — the frontier keeps shifting as new training and distillation techniques appear, and any specific step-count-vs-quality number stated here would likely be outdated within a year of writing. Treat "state of the art" claims about generation speed as inherently time-stamped.
| Symbol | Meaning |
|---|---|
| the learned velocity field | |
| a noise sample and a data sample | |
| the consistency model, mapping any trajectory point to the endpoint |
A comparison table across the whole section
| Family | Training cost | Sampling steps | Exact likelihood | Controllability |
|---|---|---|---|---|
| GAN | moderate, unstable | 1 | no | moderate (conditioning at train time) |
| VAE | low, stable | 1 | approximate (bound) | moderate |
| Normalizing flow | moderate | 1 | exact | limited (architectural constraints) |
| Diffusion (DDPM) | low, very stable | 100s-1000s | approximate | strong (guidance) |
| Diffusion (DDIM/samplers) | same as DDPM | 10s-100s | approximate | strong (guidance) |
| Flow matching / consistency | moderate-high | 1-10 | no (typically) | strong (inherits guidance) |
Code: flow matching on a 2-D toy distribution, integration steps compared
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import numpy as np
def sample_ring(n, rng):
angles = rng.uniform(0, 2 * np.pi, n)
radius = 3 + rng.normal(0, 0.1, n)
return np.stack([radius * np.cos(angles), radius * np.sin(angles)], axis=1)
class VelocityField(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(3, 64), nn.ReLU(), nn.Linear(64, 64), nn.ReLU(), nn.Linear(64, 2))
def forward(self, x, t):
return self.net(torch.cat([x, t], dim=1))
rng = np.random.default_rng(0)
data = torch.tensor(sample_ring(2000, rng), dtype=torch.float32)
model = VelocityField()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
for step in range(3000):
idx = torch.randint(0, data.size(0), (128,))
x1 = data[idx] # data sample
x0 = torch.randn(128, 2) # noise sample
t = torch.rand(128, 1) # random interpolation time
xt = (1 - t) * x0 + t * x1 # straight-line interpolation
target_velocity = x1 - x0 # constant along the straight path
pred_velocity = model(xt, t)
loss = nn.functional.mse_loss(pred_velocity, target_velocity)
optimizer.zero_grad(); loss.backward(); optimizer.step()
if step % 500 == 0:
print(f"step {step}: flow-matching loss = {loss.item():.4f}")
@torch.no_grad()
def integrate(model, n_steps, n_samples=300):
x = torch.randn(n_samples, 2)
dt = 1.0 / n_steps
for i in range(n_steps):
t = torch.full((n_samples, 1), i * dt)
x = x + model(x, t) * dt # simple Euler integration of the learned velocity field
return x
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
real_plot = sample_ring(300, rng)
for i, n_steps in enumerate([2, 5, 20, 50]):
with torch.no_grad():
samples = integrate(model, n_steps).numpy()
axes[i].scatter(real_plot[:, 0], real_plot[:, 1], alpha=0.3, label="real")
axes[i].scatter(samples[:, 0], samples[:, 1], alpha=0.3, label="generated")
axes[i].set_title(f"{n_steps} integration steps")
if i == 0: axes[i].legend()
plt.savefig("flow_matching_step_comparison.png")
See also
- Diffusion Models — the noise-prediction formulation flow matching reparameterises.
- DDPM Sampling and Guidance — the guidance mechanisms that carry over unchanged to flow-matching models.