Skip to main content

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.

Key idea

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 t[0,1]t \in [0, 1]. 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 vθ(xt,t)v_\theta(x_t, t) that describes how a point should move at time tt to travel from the noise distribution to the data distribution:

LFM=Et,x0,x1[vθ(xt,t)(x1x0)2]\mathcal{L}_{\text{FM}} = \mathbb{E}_{t, x_0, x_1} \left[ \| v_\theta(x_t, t) - (x_1 - x_0) \|^2 \right]

where xtx_t is a simple interpolation between a noise sample x0x_0 and a data sample x1x_1. 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 fθ(xt,t)f_\theta(x_t, t) 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.

SymbolMeaning
vθ(xt,t)v_\theta(x_t, t)the learned velocity field
x0,x1x_0, x_1a noise sample and a data sample
fθ(xt,t)f_\theta(x_t, t)the consistency model, mapping any trajectory point to the endpoint

A comparison table across the whole section

FamilyTraining costSampling stepsExact likelihoodControllability
GANmoderate, unstable1nomoderate (conditioning at train time)
VAElow, stable1approximate (bound)moderate
Normalizing flowmoderate1exactlimited (architectural constraints)
Diffusion (DDPM)low, very stable100s-1000sapproximatestrong (guidance)
Diffusion (DDIM/samplers)same as DDPM10s-100sapproximatestrong (guidance)
Flow matching / consistencymoderate-high1-10no (typically)strong (inherits guidance)

Code: flow matching on a 2-D toy distribution, integration steps compared

flow_matching_demo.py
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