The rigorous reference companion to the five-lecture series — every definition, theorem, and derivation in one place. From "generation is sampling" to the marginalization trick, score matching, classifier-free guidance, diffusion transformers, latent diffusion, and discrete diffusion for language.
The five-lecture Gleam series on this site teaches each idea one lecture at a time, intuition-first, with a single simulation per concept. This Veanor is the rigorous reference companion: it covers the whole set of notes — "An Introduction to Flow Matching and Diffusion Models" by Peter Holderrieth and Ezra Erives (MIT 6.S184) — with the actual definitions, theorems, step-by-step derivations, and notation you would cite. It is still intuition-first; it just refuses to skip a step.
We use the same convention as the notes throughout. Time runs from t = 0 (noise) to t = 1 (data). Objects are vectors x in d-dimensional space. The data distribution is pdata; the easy initial distribution is pinit (almost always a standard Gaussian). A subscript on a function means time-dependence: ut(x) is the vector field at time t and location x.
Chapter 13 is a one-page cheat sheet and a map to the rest of the site (the 5 Gleam lectures, the Diffusion and Flow Matching Gleams, the Architecture Atlas, and the SDE deep-dive).
Before any differential equation, we need to say precisely what "generate" means. The notes make this move in four key ideas; we restate each with its reasoning.
A computer stores numbers, so every object we generate is first flattened to a vector z in Rd (d-dimensional real space). The notes give three concrete encodings:
There is no single best dog image; there is a spectrum. Machine learning realizes this spectrum as a probability distribution over the space of images, the data distribution pdata — a density pdata : Rd → R≥0 assigning each candidate object a likelihood. A crisp dog photo gets high density; random static gets near-zero density.
The catch — and the reason the entire field exists — is that we do not know the density pdata. Nobody can write down "the probability that this exact arrangement of 196,608 pixels is a real internet image." All we get is a finite pile of samples already drawn from it.
"A dog" and "a cat" are different distributions. A conditioning variable y (a prompt, a class label, a caption) selects a guided data distribution pdata(· | y).
A terminology landmine, flagged now. The word "conditional" gets two meanings in these notes. From Chapter 4 onward, "conditional" almost always means conditioning on a clean data point z (the conditional probability path pt(x|z)). To avoid collision, the notes reserve the word guided for conditioning on a prompt y. We follow that convention strictly.
The generative recipe is: pick an easy pinit (the standard Gaussian, written N(0, Id)), then continuously transport a sample of pinit at t = 0 into a sample of pdata at t = 1. "Continuous transport" is the job of an ordinary differential equation (ODE). Three objects describe it from three angles.
A trajectory is the path of a single point, a function X : [0,1] → Rd, t ↦ Xt. A vector field assigns a velocity arrow to every location and time:
Think of ut(x) as a wind map that can change over the day. The ODE then ties the trajectory to the field: at every instant the trajectory's velocity must equal the arrow at its current location.
The first equation says "go where the arrow at your feet points." The second pins down where you start — without it the path is not determined.
So vector fields define ODEs whose solutions are flows — three names, one underlying object. The field is the wind; the ODE is the law "drift with the wind"; the flow is the resulting warp of all of space.
Blue arrows are the vector field ut(x). The orange dot starts where you click (or press Drop point) and follows the arrows — that traced path is the ODE trajectory. Switch fields to feel how the arrows dictate motion. The "contracting" field u(x) = −θx is the running example we solve by hand below.
Why you can file this away as good news. We will parameterize u with a neural network, and neural networks always have bounded derivatives. So in every case we care about, the flow exists and is unique. Theorem 3 is reassurance, not an obstacle.
Take the simplest non-trivial field, a contraction toward the origin, ut(x) = −θx with θ > 0. Claim: the flow is ψt(x0) = exp(−θt)·x0. We verify both defining properties:
Plug in numbers: θ = 1, x0 = 2 gives ψt(2) = 2 exp(−t); at t = 1, 2⁄e ≈ 0.7358 — a clean ground truth for testing a numerical integrator.
A neural-net field has no closed-form flow, so we simulate: approximate the journey by many tiny straight steps. The Euler method initializes X0 = x0 and updates
where h is the step size and n the number of steps. The derivative is the limit of a difference quotient, d⁄dt Xt ≈ (Xt+h − Xt)/h; setting that equal to ut(Xt) and rearranging gives the update. The arrow is only exactly right at the start of each step, so a finite h leaves an error that shrinks with h — Euler is first-order.
A flow model makes two changes to the ODE machinery: the field becomes a neural network uθt with parameters θ, and the start becomes random, X0 ∼ pinit. Randomness enters once, at t = 0; the journey afterward is deterministic. The goal of training is X1 ∼ pdata.
# Algorithm 1 — Sampling from a flow model with the Euler method # Input: trained vector-field network u_theta(x, t); number of steps n t = 0.0 h = 1.0 / n # step size x = sample_gaussian() # X_0 ~ p_init = N(0, I) for i in range(n): x = x + h * u_theta(x, t) # one Euler step along the learned arrows t = t + h return x # X_1, a sample of p_data
Flow models are deterministic after the start. Diffusion models add randomness at every step. The canonical source of continuous randomness is Brownian motion (also called the Wiener process, hence the symbol W — after Norbert Wiener, who taught at MIT).
A stochastic process is a random trajectory: run it twice from the same start and you get two different paths. Brownian motion W is the continuous-time limit of a random walk.
To simulate, discretize time into steps of size h and add a Gaussian kick whose variance is h. Since standard normal noise has variance 1, scaling by √h gives variance h:
An SDE is an ODE with Brownian noise stirred in. Because a random path is too jagged to differentiate, we first rewrite the ODE without derivatives. Using the difference-quotient definition:
where Rt(h) → 0 as h → 0. This restates Euler with no derivatives. Now amend it with a Brownian kick scaled by a diffusion coefficient σt ≥ 0:
This is the Euler–Maruyama update. Since the Brownian increment is √h·ε, in code it reads Xt+h = Xt + h·ut(Xt) + σt·√h·ε. Mathematicians write the same thing in compact symbolic form (shorthand for the limit of the update above):
Here ut is the drift (deterministic pull) and σt the diffusion (noise strength). Two facts keep SDEs friendly:
The cleanest non-trivial SDE is the Ornstein–Uhlenbeck (OU) process: constant linear drift ut(x) = −θx (a spring toward 0, θ > 0) and constant diffusion σ.
Two forces fight: the drift pulls in, the noise pushes out. Their balance produces a stationary distribution — a steady-state spread the process settles into and never leaves:
Sanity-check the extremes. More noise σ → wider spread (σ² on top). Stronger pull θ → tighter spread (2θ on the bottom). If σ = 0, the variance is 0 — every path collapses to the origin, recovering the contracting flow from Chapter 2.
Now assemble everything: implement the OU drift and the full Euler–Maruyama step, then verify the stationary variance.
Chapters 2–3 built the machine (ODEs/SDEs and how to simulate them) but assumed the vector field was handed to us. Flow matching is the algorithm for training the field so that simulating the ODE produces samples from pdata. We restrict to flow models: X0 ∼ pinit, dXt = uθt(Xt) dt, and we want X1 ∼ pdata. The question "how to train" is really "how do we pick θ so the endpoint lands on the data?"
The trajectory must satisfy X0 ∼ pinit and X1 ∼ pdata, but we have freedom in what distribution the points pass through at intermediate times. A probability path formalizes that freedom — a trajectory in the space of distributions.
Let δz denote the Dirac delta at z: sampling it always returns z (it is the deterministic "all mass at one point" distribution). A conditional (interpolating) probability path is a family of distributions pt(x|z) with
In words: conditioned on a fixed data point z, the path gradually converts noise into a spike on that exact z. (This is why we call it conditional — conditioned on the data point, not the prompt.)
Because of the endpoint conditions on pt(·|z), the marginal path automatically interpolates noise and data: at t = 0 every conditional path is pinit, so p0 = pinit; at t = 1 each conditional is δz, so the mixture over z ∼ pdata reassembles p1 = pdata. That is the entire point.
Almost all state-of-the-art models use the Gaussian probability path. Choose two noise schedulers αt, βt — continuously differentiable, monotonic functions with α0 = β1 = 0 and α1 = β0 = 1 — and define
Check the endpoints: at t = 0, N(0·z, 1·I) = N(0, I) = pinit; at t = 1, N(1·z, 0·I) = δz (a Gaussian with zero variance and mean z is the spike at z). Both conditions hold, so it is a valid conditional path. Sampling the marginal Gaussian path is the one-liner everyone implements:
This is just "take a data point, scale it down by αt, and add Gaussian noise of scale βt." More noise for lower t, until at t = 0 there is only noise.
Drag the time slider. Conditional mode shows pt(·|z) for one fixed data point z (orange star): a single Gaussian blob that starts wide at t = 0 and contracts onto z at t = 1. Marginal mode mixes over several data points (a ring of stars): the cloud starts as one Gaussian and splits into the data distribution. Same path, two views — exactly Figure 5 of the notes.
A probability path says what distribution the points should have at each time. But "should" is a wish — we need an actual vector field whose ODE trajectories realize it. Flow matching constructs one explicitly.
For each data point z, a conditional vector field utargett(x|z) is any field whose ODE follows the conditional path:
We can usually write this down by hand. For the Gaussian path, build the conditional flow first: ψtargett(x|z) = αt z + βt x. If X0 ∼ N(0,I), then Xt = αt z + βt X0 ∼ N(αtz, βt2I) = pt(·|z) — correct distribution. To extract the field, use the flow defining property and the chain rule (writing α̇t, β̇t for time derivatives):
Why a conditional field looks useless — and is not. Following utargett(·|z) always lands on the same fixed z — you would just re-generate a known data point. The payoff is that conditional fields are building blocks for a field that generates genuinely new samples.
Read the weight. By Bayes' rule, pt(x|z) pdata(z) ⁄ pt(x) is the posterior over clean data points z given the noisy point x — "given I see this noisy x at time t, how plausible is it that it came from z?" So the marginal field is intuitive: for every candidate z it takes the velocity that would steer toward z, and averages those velocities, weighting each by how much we believe x came from that z.
The proof rests on the continuity equation, the conservation law of probability mass. Define the divergence div(vt)(x) = ∑i ∂vit/∂xi (the net outflow of the field at a point).
Proof of Theorem 9. By Theorem 11 it suffices to show the marginal field satisfies the continuity equation. Direct calculation, with each step labeled:
The first and last lines are precisely the continuity equation for utargett. By Theorem 11 this implies Xt ∼ pt, finishing the proof. (The Fokker–Planck equation in Chapter 8 generalizes Theorem 11 to SDEs.)
We now have the training target: make the network uθt equal the marginal vector field utargett. If it does, then by Theorem 9 the endpoints X1 ∼ pdata are exactly what we want. The obvious objective is a mean-squared error, the flow matching loss (with Unif the uniform distribution on [0,1] and E expectation):
The conditional field utargett(x|z) is tractable (we wrote it in closed form in Chapter 5). So define the conditional flow matching loss, regressing against the conditional field instead:
This is computable: draw a data point z, draw a time t, sample x ∼ pt(·|z) (just noise z), evaluate the network, take an MSE against the known conditional velocity. But why should regressing the conditional field give us the marginal field we actually care about?
Proof sketch (the load-bearing step). Expand the MSE with ‖a−b‖2 = ‖a‖2 − 2a·b + ‖b‖2. The ‖utarget‖2 term has no θ, so it is a constant C1. The ‖uθ‖2 term is identical under both sampling schemes (sampling x ∼ pt equals sampling z then x ∼ pt(·|z)). The only term that mixes the network with the target is the cross term, and there the magic happens:
The intractable pt(x) in the denominator cancels the pt(x) from writing the expectation as an integral — the cross term, started with the marginal field, ends as an expectation over the conditional field. Substituting back and re-completing the square gives LFM = LCFM + C. Since C is independent of θ, the gradients coincide. □
For the Gaussian path, the conditional field is utargett(x|z) = (α̇t − (β̇t/βt)αt)z + (β̇t/βt)x, and substituting x = αtz + βtε simplifies the loss to a regression against α̇tz + β̇tε. For the CondOT path αt = t, βt = 1 − t, that target is just z − ε:
# Algorithm 3 — Flow Matching Training (Gaussian CondOT path p_t(x|z) = N(t·z, (1-t)^2)) for each mini-batch in dataset: z = sample_data() # a real data point z ~ p_data t = uniform(0, 1) # random time eps = sample_gaussian() # noise ~ N(0, I) x = t*z + (1 - t)*eps # x ~ p_t(·|z): noise the data point loss = norm(u_theta(x, t) - (z - eps))**2 # regress against z - eps grad_update(loss)
So far our central object was the vector field ut(x). Diffusion models take a different but equivalent perspective built on score functions. This chapter rephrases everything in that language — and shows the two are linked by a single conversion formula.
For any distribution q(x), the score function is the gradient of the log-density, ∇ log q(x). It points in the direction of steepest ascent of log-likelihood — "which way should I nudge x to make it more probable?" At a mode it is zero; in the tails it points back toward the bulk.
Returning to probability paths, define the conditional score ∇ log pt(x|z) and the marginal score ∇ log pt(x). They are related just like the vector fields:
The same posterior weight appears. The derivation uses ∂y log y = 1/y and the chain rule: ∇ log pt(x) = ∇pt(x)/pt(x) = [∫ ∇pt(x|z)pdata(z)dz]/pt(x), and writing ∇pt(x|z) = pt(x|z)∇ log pt(x|z) gives the weighted average.
For the Gaussian path pt(x|z) = N(αtz, βt2I), the log-density is −‖x − αtz‖2/(2βt2) plus a constant in x. Its gradient is the clean linear form:
Notice: the score points from the noisy point back toward the (scaled) clean point, scaled by 1/βt2. Since x = αtz + βtε, this equals −ε/βt — the score is, up to scale, the negative of the noise that was added. That is why learning the score and "predicting the noise" are the same task (next chapter).
This is striking: once you have learned the marginal vector field, you have also learned the score — and vice versa. Many diffusion models therefore learn the score instead of the field; for Gaussian paths it makes no difference. (To carry the conditional identity to the marginal one, integrate against the posterior weight and use that the posterior integrates to 1.)
A third equivalent quantity is the denoiser Dt(x) = E[z | x] — the expected clean data point given the noisy x. Field, score, and denoiser are all linear reparameterizations of this posterior mean, so any one recovers the others. Predicting the denoiser is often preferred for numerical stability; models that do are called denoising diffusion models.
Now confirm the Gaussian conditional-score formula numerically — verify the analytic gradient against a finite-difference of the actual log-density, with no spoiler of the formula in the check.
Two jobs remain. First, learn the marginal score directly (for paths where we cannot just convert from the field). Second, turn the deterministic ODE sampler into a stochastic SDE sampler — which often produces better, self-correcting samples.
To approximate ∇ log pt(x), use a score network sθt. As with flow matching there is an intractable score matching loss (regress against the marginal score) and a tractable denoising score matching loss (regress against the conditional score):
For the Gaussian path, plugging ∇ log pt(x|z) = −ε/βt into LCSM shows the network is learning to predict the noise that corrupted the data. This is unstable as βt → 0 (the 1/βt2 blow-up you saw in the lab), so DDPM dropped the constant and reparameterized into a noise predictor εθt:
This is the loss behind the original Denoising Diffusion Probabilistic Models. Algorithm 4 is Algorithm 3 with "predict the velocity" replaced by "predict the noise" — noise a data point, regress against ε.
The marginal field gives an ODE that follows the path. Can we follow the same marginal path with an SDE? Yes — and we get to choose the noise level after training.
Why this is remarkable. One trained model gives you an entire family of samplers indexed by σt. At σt = 0 you recover the deterministic probability-flow ODE; at σt > 0 you get stochastic sampling that injects fresh noise (the score term steers it back), which can self-correct accumulated errors. In theory any σt works; in practice training error and finite step sizes mean there is an empirically best σt.
The proof generalizes the continuity equation to SDEs. Define the Laplacian Δwt(x) = ∑i ∂2wt/∂xi2 = div(∇wt)(x).
Proof of Theorem 17 (sketch). Start from the continuity equation for the ODE field, ∂t pt = −div(pt utargett). Add and subtract (σt2/2)Δpt. Rewrite one copy using Δpt = div(∇pt) and ∇ log pt = ∇pt/pt, so (σt2/2)Δpt = div(pt·(σt2/2)∇ log pt). Folding it into the divergence gives ∂t pt = −div(pt[utargett + (σt2/2)∇ log pt]) + (σt2/2)Δpt — the Fokker–Planck equation for the SDE in Theorem 17. By Theorem 19, Xt ∼ pt. □
Everything so far was unguided: the model returns some sample of pdata. We almost always want a prompt: sample pdata(·|y) for a text prompt or class y. Recall the terminology: "conditional" means conditioning on a clean data point z; guided means conditioning on a prompt y.
The simplest approach: feed y to the network as an extra input, uθt(x|y), and train exactly as before but with data pairs (z, y) ∼ pdata. The guided conditional flow matching objective is
The only change from the unguided loss is sampling (z,y) instead of just z — in PyTorch, a dataloader returning both images and prompts. The prompt does not change the conditional path or conditional field; it just enters the network. In theory this gives faithful samples from pdata(·|y).
For Gaussian paths, write the guided field via the guided score, utargett(x|y) = at ∇ log pt(x|y) + bt x. By Bayes' rule (gradient in x, so ∇ log pt(y) = 0):
Substituting gives utargett(x|y) = utargett(x) + at ∇ log pt(y|x): the guided field is the unguided field plus the gradient of a classifier log pt(y|x) (the log-likelihood of the prompt given the noisy image). Classifier guidance scales that term by a guidance scale w > 1:
It works, but needs a second network (a classifier on noisy data), and for high-dimensional y (text) pt(y|x) is hard to learn. Note w ≠ 1 makes this a heuristic — it is no longer the true guided field.
The fix that runs every modern image/video model: get the same amplifying effect without a separate classifier. Substitute the Bayes identity ∇ log pt(y|x) = ∇ log pt(x|y) − ∇ log pt(x) into the classifier-guidance formula and collect terms. The score and the btx pieces recombine into whole vector fields, leaving the clean linear combination:
One model, not two. The trick is to augment the label set with a special null label ∅ meaning "no conditioning," and set utargett(x) = utargett(x|∅). During training (Algorithm 5) we drop the real label to ∅ with probability η, so the same network learns both the guided and unguided fields. At inference we evaluate both branches and combine them with the formula above.
The teal dot is the unguided target (population center, prompt off); the orange star is the guided target for prompt y. The purple marker is the CFG result (1−w)·unguided + w·guided. Drag w: at w = 1 the result sits on the guided target; as w grows it extrapolates past it, sharpening prompt adherence (and eventually over-saturating). Almost every AI image you see uses w ≥ 4.
Now build the CFG combination on a 2D toy and confirm the geometry: at w = 1 you recover the guided field; larger w pushes strictly further toward the prompt.
The math above never said how the network uθt(x|y) is built. It has three inputs — a vector x in Rd, a prompt y, and a scalar time t — and one output, a vector uθt(x|y) in Rd of the same shape as x (it is a velocity per coordinate). For toy distributions an MLP over the concatenation of x, y, t works. For images and video we need specialized architectures.
Time. Concatenating the raw scalar t works for toys, but in practice t is lifted to a higher-dimensional Fourier feature embedding so the network can capture high-frequency time dependence:
with geometrically spaced frequencies wi from wmin to wmax. Because sin² + cos² = 1, the embedding has unit norm. The exact form is not sacred — it is just a convenient normed embedding of the scalar time.
Class labels. For a discrete label in {0,…,N}, learn one embedding vector per class and look it up — these embeddings are part of θ.
Text. For a text prompt, use a frozen, pre-trained encoder. CLIP (Contrastive Language-Image Pre-training) learns a shared image–text embedding space and gives a single coarse vector y = CLIP(yraw). To preserve word-level detail one also uses a pre-trained transformer (e.g. T5) to get a sequence of embeddings PromptEmbed(yraw) of shape S × k. Models often combine several pretrained text encoders.
The U-Net is a convolutional network whose input and output both have image shape — ideal for parameterizing x ↦ uθt(x|y). It funnels an image down through encoders (channels grow, height/width shrink) to a small latent, processes it in a bottleneck, then expands back through decoders to image shape:
U-Nets dominated the early diffusion literature; modern variants add attention layers inside the encoder/decoder stacks.
The diffusion transformer applies the attention mechanism instead of convolution. Building on Vision Transformers, it patchifies the image, embeds each patch as a token, processes the token sequence with transformer layers, then depatchifies back to image shape:
Inside a DiT block. Three operations: (i) self-attention over patches (each patch attends to all others, the same scaled dot-product attention softmax(QKT/√dh)V with multiple heads); (ii) cross-attention to the prompt tokens (queries from patches, keys/values from y); and (iii) time conditioning via adaptive normalization (AdaLN), where the time embedding produces per-channel scale and shift parameters (γ, β) = g(t̃) that modulate normalized activations, AdaNorm(x) = (1 + γ) ⊙ Norm(x) + β. Class-conditioned DiTs (as in the course lab) usually drop cross-attention in favor of time-and-class AdaLN.
Modeling images directly in pixel space is brutal: a 1024×1024 RGB image is d ≈ 3 million dimensions, and a flow model's output must be just as large as its input. The fix is to model in a compressed latent space.
An autoencoder pairs an encoder μφ : Rd → Rk with a decoder μθ : Rk → Rd, with k « d (e.g. downsample 1024 to a 16×16-ish latent). Trained on the reconstruction loss E[‖μθ(μφ(x)) − x‖2], it learns to compress and rebuild. But there is no control over the latent distribution platent(z) — compression may turn an easy data distribution into a wild latent one that is hard to model generatively.
A VAE makes the encoder/decoder probabilistic — qφ(z|x) = N(μφ(x), σφ2(x)), pθ(x|z) = N(μθ(z), σθ2) — and adds a regularizer that pushes the latent distribution toward a clean Gaussian prior pprior = N(0, Ik). The objective combines reconstruction with a prior term weighted by β:
The first term forces latents to decode back to data; the second forces the per-example encoding to look Gaussian, so the aggregate latent distribution is Gaussian-like and easy to model. The KL divergence DKL(q ‖ p) = Eq[log(q/p)] measures dissimilarity between distributions; it is ≥ 0 and zero iff q = p. For isotropic Gaussians it has a closed form penalizing the encoder mean for being non-zero and the variance for being non-one.
Stable Diffusion 3. Uses exactly the conditional flow matching objective from Chapter 6 (the authors tested many alternatives and found flow matching best), with classifier-free guidance training, inside the latent space of a pretrained autoencoder. Text conditioning combines three embeddings — two CLIP encoders plus the sequential output of Google's T5-XXL — fed to a multi-modal DiT (MM-DiT) that attends to both image patches and text tokens. The largest model has 8 billion parameters; sampling uses 50 Euler steps with a CFG weight between 2.0 and 5.0.
Meta Movie Gen Video. Data are videos in RT×C×H×W. It uses conditional flow matching with the straight-line schedulers αt = t, βt = 1 − t, in the latent space of a temporal autoencoder (TAE) that compresses time and space (with temporal tiling for long videos). The backbone is a DiT patchified over time and space, with self-attention among patches and cross-attention to text. It uses three text encoders (UL2 for reasoning, ByT5 for character-level detail, MetaCLIP for shared image–text space). The largest model has 30 billion parameters.
Text is not a point in Rd — it is a sequence of discrete tokens. There is no continuous diffusion process on a discrete set (SDEs need a real-valued state). The principles transfer anyway, with one substitution: replace ODEs/SDEs with continuous-time Markov chains (CTMCs).
Let the vocabulary be V = {v1,…,vV} (an alphabet, a token set, or the 4 DNA bases). The state space is S = Vd — all sequences of length d. A discrete generative process is a random trajectory Xt in S that is Markov: the future depends only on the present, p(Xt+h | Xt, past) = p(Xt+h | Xt). Such a process is a continuous-time Markov chain.
In discrete space you cannot move "in a direction"; you can only jump between states. The analogue of a vector field is a rate matrix Qt(y|x) — the instantaneous rate of jumping from state x to state y. It obeys two conditions:
Theorem 33 (existence & uniqueness): every bounded, time-continuous rate matrix corresponds to a unique CTMC — so we may parameterize Qθt with a network and trust a chain exists. We simulate a CTMC with a discrete Euler step: for small h, pt+h|t(y|x) ≈ 1y=x + h·Qt(y|x) — a valid categorical distribution we sample from each step.
The recipe is identical to continuous flow matching: (1) build a probability path interpolating noise and data; (2) derive a conditional and marginal rate matrix; (3) learn the marginal rate matrix simulation-free.
A discrete conditional probability path pt(x|z) again satisfies p0(·|z) = pinit, p1(·|z) = δz, and the marginal path is the data-weighted mixture pt(x) = ∑z pt(x|z) pdata(z). The standard choice is the factorized mixture path: independently per token, with probability κt keep the clean token zj and with probability 1 − κt replace it with a noise token, where the scheduler runs κ0 = 0 to κ1 = 1. Unlike the Gaussian path it does not transport mass — it fades one distribution out and another in (there is no direction in discrete space).
So training reduces to cross-entropy. The discrete flow matching loss is the token-wise negative log-likelihood of the clean tokens given the noised sequence:
The parallel to continuous flow matching is exact: there, training reduced to regression against a velocity; here, it reduces to classification of the clean tokens. A standard sequence-to-sequence transformer with a softmax per position is enough.
You now hold the whole document — from "generation is sampling" to masked diffusion language models. Here it is on one page.