Train a velocity field to transport noise to data along straight interpolation paths, a simulation-free objective that generalizes diffusion and samples well in few steps.
Equation
Linear (rectified/OT) path between noise and data :
The conditional velocity is constant along each path:
Objective (conditional flow matching), whose gradient matches that of the intractable marginal objective:
Sampling integrates the ODE from to .
From scratch
import torch
def fm_loss(model, x1): # x1: (b, ...) data
x0 = torch.randn_like(x1) # noise
t = torch.rand(x1.shape[0], device=x1.device)
tb = t.view(-1, *[1] * (x1.dim() - 1)) # (b, 1, 1, 1)
xt = (1 - tb) * x0 + tb * x1
return (model(xt, t) - (x1 - x0)).pow(2).mean() # target velocity
@torch.no_grad()
def euler_sample(model, shape, steps=25, device='cpu'):
x = torch.randn(shape, device=device) # t = 0
dt = 1.0 / steps
for i in range(steps):
t = torch.full((shape[0],), i * dt, device=device)
x = x + model(x, t) * dt # explicit Euler
return x # t = 1, dataNotes
- Equivalent to diffusion under a reparameterization: the linear path corresponds to a specific variance-preserving-like schedule, and -style targets relate the two. The practical difference is schedule and loss weighting, not the model class.
- Straighter paths mean lower discretization error, so Euler with 10 to 30 steps is usually enough; rectified flow reflows the model on its own samples to straighten further, reaching 1 to 4 steps.
- Sampling timestep distribution matters: logit-normal sampling (emphasizing mid ) noticeably beats uniform at scale.
- Sign/direction conventions vary ( noise vs data); check which end is which before porting a sampler.