Flow Matching & Diffusion Models · Lecture 5 of 5 · MIT 6.S184

Discrete Diffusion & Discrete Flow Matching

Text is discrete — you cannot add Gaussian noise to a token. This lecture carries flow and diffusion to discrete data using continuous-time Markov chains, and builds a diffusion language model that fills in masked words in parallel.

Prerequisites: Lecture 4 (fmdm-04) — or at minimum the continuous picture from Lecture 1 (fmdm-01): vector field, ODE, the Euler step, and "generation = sampling." Plus comfort with probability vectors and a little Python.
10
Chapters
3
Simulations
3
Code Labs

Chapter 0: Why — You Cannot Add Noise to a Word

The whole lecture in one sentence. Everything in Lectures 1–4 lived in continuous space: you nudged a vector of pixels by a tiny velocity arrow, or jostled it with a sprinkle of Gaussian noise. Text does not live there. A token is a discrete symbol — "cat", "the", a period — drawn from a finite vocabulary. You cannot add half a Gaussian to "cat" and get something one-third of the way toward "dog." So we rebuild the same flow-and-diffusion recipe on a foundation that fits discrete data: the continuous-time Markov chain. The payoff is a diffusion language model that generates a whole sentence by un-masking words in parallel, in any order — a genuine alternative to left-to-right GPT.

For four lectures we built one machine. Start from an easy distribution (a Gaussian blob). Transport it continuously toward the data distribution by following a differential equation — an ODE for a flow model, an SDE for a diffusion model. We learned to simulate that with tiny Euler steps, and (in Lectures 2–3) to train the velocity arrows by flow matching and score matching.

Now point that machine at language. Immediately it breaks. The velocity arrow in an ODE answers "which way, how fast?" — a direction in real space. There are no directions between symbols. The Gaussian kick in an SDE adds a small real number to each coordinate. There is no "small real number" you can add to the token "the." The continuous-state assumption is baked into every formula we wrote.

The honest statement (from the slides). There is no SDE and no ODE on a discrete state space. You cannot literally diffuse text. What survives is the learning principle behind flow matching and denoising — "interpolate a path from noise to data, then learn to reverse it." That principle is more general than continuous space. The mathematical object that carries it to discrete data is the continuous-time Markov chain (CTMC): a process that, instead of drifting, makes sudden random jumps between states at controlled rates.

Here is the analogy that will carry you through the lecture. A continuous diffusion model is like a balloon drifting on the wind — smooth, continuous motion. A discrete model is like a frog on a grid of lily-pads. The frog cannot drift halfway between pads; it sits on one pad, and at random instants it jumps to a neighbor. We get to set the rate at which it tends to jump in each direction. Generation becomes: design the jump-rates so that a frog starting on a "noise" pad hops, over time, onto a "real sentence" pad.

Where this sits. This is Lecture 5 (the final lecture) of MIT's 6.S184: Generative AI with Stochastic Differential Equations (Holderrieth & Shprints). The series: (1) Flow & Diffusion Models → (2) Flow Matching → (3) Score Matching & Guidance → (4) Architectures & Latent Spaces → (5) Discrete Diffusion & Discrete Flow Matching. By the end you will hand-build the forward corruption process, the CTMC Euler step, and a masked-denoising sampler — the literal skeleton of models like LLaDA.

We will keep the running example tiny and concrete: a four-token "sentence" over a small alphabet, corrupted into [MASK] symbols and then reconstructed. Tiny enough to compute every probability by hand; structurally identical to a billion-parameter diffusion LLM.

Why can't we use the continuous diffusion machine from Lectures 1–4 directly on text?

Chapter 1: The Discrete State Space — the Analogue of Rd

In the continuous world a sample was a point in Rd — d real numbers. We need the discrete replacement for that, and it has exactly two ingredients.

Vocabulary and sequences. Fix a vocabulary V = {v1, …, vV} — the finite set of allowed symbols (an alphabet, a set of word-pieces, the four DNA bases). A data point is a sequence of length d: x = (x1, …, xd), where each position xj holds one symbol from V. The set of all such sequences is the state space S = Vd. That is the discrete analogue of Rd.

Read the two numbers carefully — they will haunt us:

The size explosion that shapes everything. How many sequences are there? Each of the d positions can independently be any of V symbols, so |S| = Vd. For language that is 50,0001024 — a number so large the universe has no room to list it. The continuous case had its own infinity (Rd is uncountable), but here the explosion bites in a specific way: we can never store a probability for each whole sequence. Every algorithm in this lecture is designed to dodge Vd. Hold this thought — it is the reason for the "factorized" trick in Chapter 3.

A distribution over sequences

Just as before, generation is sampling. The data distribution pdata is now a probability mass function over S: it assigns to each possible sequence z a non-negative weight pdata(z), and these sum to 1 over all sequences. "All texts on the internet" is such a distribution — grammatical, sensible sentences carry high mass; keyboard mash carries near-zero mass. As always, we never see pdata; we only get a dataset of samples z ∼ pdata (a corpus).

Concrete toy we will reuse. Vocabulary V = {A, B, C} (so V = 3), sequence length d = 4. A data point is a length-4 string like ABCA or CCAB. The state space has |S| = 34 = 81 possible sequences. Small enough that, in Chapter 4's code lab, we list and reason about them directly — impossible for real text, perfect for learning.

The one new token: [MASK]

One small but pivotal move. For the kind of model we build, we extend the vocabulary with a special extra symbol, [MASK], that means "this position has been erased — its real symbol is hidden." So the working alphabet becomes V ∪ {[MASK]}. A partially-corrupted sentence is then a mix of real symbols and [MASK] holes, e.g. A·[MASK]·C·[MASK]. This single addition is what makes the whole machine concrete and is the heart of masked diffusion language models (Chapter 7).

With vocabulary size V and sequence length d, how many possible sequences are there, and why does that number matter so much?

Chapter 2: Continuous-Time Markov Chains — Jumps Instead of Drift

We have a discrete state space S. We need the discrete replacement for "transport a point continuously through time." In the continuous world that job belonged to the ODE/SDE. Here it belongs to the continuous-time Markov chain (CTMC).

What a CTMC is. A CTMC is a random trajectory Xt over the discrete states S, indexed by continuous time t in [0, 1]. The trajectory sits on one state, then at a random instant jumps to another state, sits there, jumps again. It is a step function in time — flat, then a sudden vertical jump, then flat again. Compare: an ODE trajectory is a smooth curve. A CTMC trajectory is a staircase whose step-times and step-targets are random.

"Continuous-time" refers to when the jumps can happen — at any real-valued instant, not on a fixed clock tick. "Markov chain" means the process has no memory: where it jumps next depends only on where it is now, not on the whole history of how it got there.

The Markov (memoryless) property, in words. Given the present state Xt, the future is independent of the entire past. Knowing you are currently on lily-pad C tells you everything; the sequence of pads you visited earlier adds nothing. The continuous ODEs and SDEs were Markov too (the next step used only the current point). The new word "chain" just signals that the state is now discrete.

Transition probabilities: the thing a CTMC actually computes

How do we describe a CTMC quantitatively? By its transition probabilities: for a small elapsed time h, the probability of being in state y at time t+h given you are in state x at time t. Together with the initial distribution (where you start), these probabilities fully determine the process. They are the discrete analogue of "the flow tells you where you end up."

But here is the catch we already met in the continuous world: for any interesting process we do not have a closed-form formula for those transition probabilities. In Lecture 1 we did not have the flow map either — we had the vector field and we simulated. We need the discrete analogue of a vector field: a local rule that says, instant by instant, how fast the process tends to jump where. That object is the rate matrix, and it is the entire subject of Chapter 3.

A CTMC trajectory — a frog hopping between three lily-pads

Three states (A, B, C). The colored staircase is one CTMC trajectory: flat stretches (sitting on a pad) punctuated by sudden jumps to a neighbor. Press New trajectory for fresh randomness — same rules, different path, because the dynamics roll dice. Raise the jump rate and the staircase gets busier (more, sooner jumps). Notice: between jumps nothing moves — there is no drift, only sudden teleports.

jump rate3.0
How does a CTMC trajectory differ from the smooth ODE trajectory of Lecture 1?

Chapter 3: The Rate Matrix Q — the Discrete Vector Field

The vector field ut(x) was the soul of the continuous model: at every location and time it gave a velocity. Its discrete replacement is the rate matrix (also called the generator), written Qt. It answers the discrete version of "which way, how fast" — namely "from state x, how fast do I tend to jump to each other state y?"

Definition of the rate matrix. For each ordered pair of states (x, y) and each time t, Qt(y | x) is the instantaneous rate of jumping from x to y. Bigger rate = jumps happen sooner and more often. Two rules make it legal:
  • (1) Off-diagonal rates are non-negative: Qt(y | x) ≥ 0 for y ≠ x. A jump-rate cannot be negative — "not jumping" is just rate 0.
  • (2) Each row sums to zero: the diagonal entry Qt(x | x) = −∑y≠x Qt(y | x). The rate of "staying" is set to cancel the total rate of "leaving."

The diagonal looks strange — a negative "rate of staying." Read it as bookkeeping, not a real jump. Rule (2) says: the total rate of all outgoing jumps from x, plus the diagonal, equals zero. Equivalently, the diagonal is minus the total outflow. This is exactly the constraint that keeps probability conserved — nothing is created or destroyed, mass only moves between states.

Why "rows sum to zero" is the load-bearing rule. Imagine the process as a distribution — a probability vector p over states, summing to 1. Over a tiny step h, mass flows out of x at rate (total outflow) and into x from neighbors. If every row of Q sums to zero, the total probability is unchanged: every unit that leaves x arrives somewhere. Break this rule and the probabilities stop summing to 1 — your "distribution" silently leaks or inflates. In Chapter 4's code lab the TODO-gap is exactly this: a Q whose rows don't sum to zero, and you will watch the total probability drift away from 1.

From rates to one Euler step

The vector field defined an ODE we Euler-stepped: Xt+h = Xt + h·ut(Xt). The rate matrix defines a CTMC, and the discrete Euler step is just as simple. Over a tiny time h, the probability of having jumped from x to a different state y is approximately rate×time:

pt+h | t(y | x) ≈ δy=x + h·Qt(y | x)

Here δy=x (the Kronecker delta) is 1 when y equals x and 0 otherwise — it says "with no jump you stay put." The term h·Qt(y | x) is the small probability of jumping to y. Read it as: "each tiny instant, the chance of a particular jump is its rate times the elapsed time." For small h this is a valid probability distribution over the next state, and we sample from it. That single line is the discrete Euler step — the engine of every CTMC sampler.

Worked number. Three states A, B, C. Suppose from A the rates are Q(B | A) = 2 and Q(C | A) = 1, so total outflow is 3 and the diagonal is Q(A | A) = −3. Take a step h = 0.1. The jump probabilities are: to B, 0.1×2 = 0.2; to C, 0.1×1 = 0.1; stay at A, 1 + 0.1×(−3) = 1 − 0.3 = 0.7. Check: 0.2 + 0.1 + 0.7 = 1.0. ✓ The row-sum-zero rule is precisely what made these add to 1.

The exponential explosion, and the factorized fix

Now the Vd problem from Chapter 1 strikes. A full rate matrix has one entry per ordered pair of states — that is |S|2 = V2d numbers. Utterly impossible to store. So real CTMC models impose a sparsity constraint: they are factorized.

Factorized rate matrix. Only allow jumps that change one position at a time. Two sequences are neighbors if they differ in at most one token. A factorized Q sets the rate to zero for any jump that would change two or more positions at once. So the model only ever has to output, for each position j and each candidate symbol v, a single rate "swap position j to v." The output shape becomes d × V — linear in length, not exponential. (A transformer with output dimension V per position does exactly this.)
Neighbor check (from the slides). Over alphabet {x, y, z} as single-token states: x and y are neighbors (differ in the one position). For length-2 sequences, AB and AC are neighbors (only position 2 changed); AB and CC are not (two positions changed) — a factorized model forbids that double jump in a single instant. Multi-position changes still happen over the full trajectory, just never in one infinitesimal step (their probability is order h², negligible).
In a valid rate matrix, why must each row sum to zero (diagonal = minus the total outflow)?

Chapter 4: Forward Corruption — Fading Data Into Masks

The continuous diffusion model needed a forward / noising process: run it forward and any data point decays into a simple Gaussian. We need the discrete analogue — a process that fades a clean sentence into pure "noise" over time t from 0 to 1. The cleanest version is masking, also called the absorbing-state process.

The masking forward process. Run time from t = 1 (clean data) back toward t = 0 (pure noise) — or equivalently think of a corruption level that grows as we move away from data. At each level, every token independently has some probability of being replaced by [MASK]. Once a token is masked it stays masked — [MASK] is an absorbing state, a lily-pad with no way out. So as corruption grows, more and more positions turn into [MASK] holes, until the whole sentence is [MASK][MASK]…[MASK]: the noise distribution.

Let a scheduler κt control how much information survives at time t. By convention κ runs from κ = 0 (everything destroyed, all [MASK]) at t = 0 up to κ = 1 (everything intact) at t = 1, increasing monotonically. The probability that a given clean token zj has survived (still shows its true value) at time t is κt; the probability it has been masked is 1 − κt.

P(token survives at time t) = κt,     P(token is masked) = 1 − κt
The crucial difference from continuous diffusion (from the slides). A Gaussian forward process moves probability mass smoothly through space — the cloud drifts and spreads. The masking process does not move anything through space. There is nowhere to move to. Instead it teleports mass: it fades out the true symbol and fades in [MASK]. Each token is a mixture — with weight κt it shows the data symbol, with weight 1 − κt it shows noise. This is the factorized mixture path: a per-token mixture that "fades in one distribution and fades out another." Same role as Gaussian noising, completely different mechanism.

Why this is a valid probability path

Recall the two endpoints a forward process must hit. At t = 1, κ = 1, so every token survives with probability 1 — the distribution is the clean data point z (all mass on z). At t = 0, κ = 0, so every token is masked — the distribution is the fixed noise point (all [MASK]). In between it interpolates: a sentence partly real, partly holes. That is exactly the discrete analogue of "pinit at one end, pdata at the other," which is all a probability path needs to be.

Worked number with the toy. Clean sentence z = ABCA (d = 4). Take the simplest linear scheduler κt = t, and evaluate at t = 0.25. Then survival probability is 0.25 and mask probability is 0.75 per token. Expected number of masked tokens = d×(1 − κt) = 4×0.75 = 3 — on average 3 of the 4 positions are holes at t = 0.25. At t = 0.75: 4×0.25 = 1 masked. At t = 1: 4×0 = 0 (clean). At t = 0: 4×1 = 4 (fully masked). The expected count of [MASK]s falls smoothly from 4 down to 0 as t grows — that monotone fade is the signature of the process.
Watch a sentence fade into masks as corruption grows

The clean sentence sits at the top. Drag the slider to set the survival level κ (right = clean, left = fully masked). Each token independently shows its true symbol with probability κ (teal) or a mask hole otherwise (dim). Press Re-corrupt to roll the dice again at the same level — the pattern of holes is random, but the expected count follows d×(1−κ). The bar reports masked count vs. the theoretical expectation.

survival κ0.50

Now build the forward process yourself and confirm the expected mask-count law.

In the masking forward process with survival schedule κt, what is the expected number of masked tokens in a length-d sentence at time t?

Chapter 5: The Denoising Target — Discrete Flow Matching's Big Reveal

We can corrupt data into masks. To generate, we must run the process backward — turn masks into real tokens. As in the continuous case, we don't learn the reverse process directly; we learn a local object (there it was the marginal vector field / score) and the reverse follows. Here that object is the marginal rate matrix, and discrete flow matching delivers a stunning simplification of what we actually have to learn.

The recipe, mirrored from continuous flow matching

Same three steps as Lecture 2.
  • (1) Build a probability path from noise to data — the factorized mixture path of Chapter 4.
  • (2) Find the rate matrix that follows it. First a conditional rate matrix (per known data point z), then a marginal one via the marginalization trick.
  • (3) Learn the marginal rate matrix simulation-free, by a simple supervised loss.

The conditional rate matrix: jump straight to the answer

Fix a known clean target z. What rate matrix Qz drives the masking path for that z? Astonishingly simple: a masked token should jump only to its true symbol zj, and an already-correct token should not move. In words: "if a position is wrong (masked), un-mask it to the correct token; if it's already right, leave it." The rate of that correcting jump rises as t → 1 (set by the schedule), reflecting the urgency to finish.

The marginalization trick (Theorem 36, the heart of the lecture). We don't know z at generation time — that's what we're trying to produce. The marginal rate matrix averages the conditional ones over our belief about z given the current noisy sequence x. For the factorized mixture path it collapses to a beautifully clean form: the marginal rate for jumping position j to symbol vi is proportional to p1|t(zj = vi | x) — the model's probability that the true symbol at position j is vi, given the whole noisy sequence x.
Qt(vi, j | x) = (κ̇t ⁄ (1 − κt)) · (p1|t(zj = vi | x) − δxj=vi)

Unpack it. κ̇t (read "kappa-dot") is the time-derivative of the scheduler — how fast information is being added; the prefactor κ̇t⁄(1−κt) is just a time-dependent speed. The meat is the bracket: it pushes mass toward the model's predicted clean symbol p1|t(· | x) and away from the current token (the δ term). Everything in the rate matrix is known except that one probability — the network's guess of the clean token.

The reveal: generation reduces to classification. The marginal rate matrix is just a reparameterization of p1|t(zj | x) — "given the noisy sequence, what was the original token at each position?" That is exactly a per-position classifier: for each masked slot, predict a probability distribution over the vocabulary. So the entire generative model is a denoiser: feed it the corrupted sentence, it outputs, for every position, a softmax over V symbols guessing the clean token. In continuous flow matching the training target collapsed to simple regression; here it collapses to simple classification.

The Discrete Flow Matching loss

Training is now ordinary supervised learning. Take a clean sentence z, pick a random time t, corrupt it to a noisy x by the masking process, and ask the network to recover each original token. Score it with the standard cross-entropy (negative log-likelihood) per position, summed over the sequence:

LDFM(θ) = Ez, t, x [ ∑j=1..d −log pθ1|t(zj | x) ]

Read it: average over data sentences z, random times t, and noisy versions x, of the summed per-token negative-log-probability the network assigns to the true tokens. No simulation during training — just "corrupt, predict the clean tokens, penalize wrong guesses." This is the discrete analogue of the flow-matching regression loss, and it is what billion-token diffusion LLMs minimize.

Data-flow check (Concept + Realization). Input: a noisy sequence x of d token-ids (some are [MASK]) plus the scalar time t. The network (a transformer over length d) outputs logits of shape d × V — for each position, a score per vocabulary symbol. A softmax per row turns that into pθ1|t(· | x)j, the posterior over the clean token at position j. The loss only ever asks: did you assign high probability to the real token zj? That d × V output is linear in length — the factorization (Chapter 3) is what makes this fit in memory instead of Vd.
# Discrete Flow Matching training step (one iteration)
z = sample_data()                  # a clean sequence (z_1,...,z_d)
t = uniform(0, 1)                  # random time
kappa = schedule(t)                # survival prob in [0,1]
x = z.copy()
for j in range(d):              # forward corruption (parallel over j)
    if random() < (1 - kappa):
        x[j] = MASK                # mask this token
logits = net(x, t)                 # shape d x V  (the denoiser)
loss = cross_entropy(logits, z)    # predict the CLEAN tokens z
loss.backward(); opt.step()
After the marginalization trick, what does the neural network in a discrete flow matching / diffusion model actually have to learn?

Chapter 6: The Reverse Process — Un-masking in Parallel

We have a trained denoiser pθ1|t(· | x). Now run the CTMC forward in generation time (from the all-mask noise toward data) using the marginal rate matrix. Concretely this means: repeatedly take a noisy sequence, ask the denoiser to guess the clean tokens, and use those guesses to turn some masks into real tokens. That is the discrete Euler step (Chapter 3) wired to a learned rate.

The reverse / generation loop. Start fully masked: x = [MASK][MASK]…[MASK]. Loop over n small steps in time:
  • Predict: run the denoiser on the current x to get, for each position, a posterior over the clean token.
  • Jump (per position, in parallel): using the discrete Euler step probabilities δy=x + h·Qt(y | x), sample whether each masked position un-masks this step, and if so to which symbol (drawn from the posterior).
  • Commit & advance time: keep the un-masked tokens fixed (absorbing — once revealed, they don't re-mask), increment t, repeat.
After n steps every position has been revealed: x is a full sentence X1 ∼ pdata.

The word parallel is the whole point. Every position is updated independently and simultaneously in each step (the factorized rate matrix allows this). One forward pass of the network produces all d posteriors at once; we then un-mask many positions in the same step. Contrast with an autoregressive LLM, which must produce token 1, then condition on it to produce token 2, and so on — strictly one at a time, left to right.

Confidence-ordered un-masking — the practical sampler. A robust and widely-used strategy: at each step, un-mask the positions the denoiser is most confident about (highest posterior probability), and leave the uncertain ones masked for now. Why? Committing to your surest guesses first gives the network more context to resolve the harder positions on the next pass — a self-reinforcing cascade. Early steps fill in the "obvious" words (articles, the easy nouns); later steps resolve the ambiguous ones with the easy ones now visible. The order is emergent, not fixed left-to-right.
Why this is "iterative refinement." Each pass sees a less-corrupted sentence than the last, so its guesses improve. The same machinery also lets you edit: hold some tokens fixed, mask others, and let the model re-fill only the holes — in-filling and revision come for free, which strict left-to-right generation cannot do.
Parallel un-masking — rebuilding a sentence step by step

Start fully masked. Press Un-mask step to run one reverse step: a few of the most-confident positions reveal their token (teal), chosen by a toy denoiser that scores the true token highest. Watch the holes fill in out of order — high-confidence words first — not left-to-right. Reset to re-mask. The bar tracks how many positions are revealed vs. how many steps you've taken; fewer steps reveal more per step (coarser, riskier), more steps reveal more gradually.

reveals / step2

Now implement the reverse sampler yourself: iteratively un-mask the lowest-confidence-first... no — the highest-confidence positions, committing each prediction, until the exact target is reconstructed.

What makes the reverse (generation) process of a diffusion language model fundamentally different from a left-to-right autoregressive LLM?

Chapter 7: Sampling a Diffusion Language Model, End to End

Let's assemble the full pipeline and lay it next to the autoregressive (GPT-style) approach it competes with. A masked diffusion language model (MDLM) is the special, dominant case where the noise distribution is "all [MASK]" — the initial point is the fully-masked sequence.

Algorithm — sampling a factorized CTMC model

# Sample from a masked diffusion language model
# Input: trained denoiser net(x, t) -> per-position posterior; steps n
t = 0.0
h = 1.0 / n
x = [MASK] * d                       # X_0: fully masked (the noise dist.)
for i in range(n):
    post = net(x, t)                 # shape d x V: clean-token posteriors
    for j in range(d):           # PARALLEL across positions
        if x[j] == MASK:
            # discrete Euler step: small prob to un-mask this step
            if should_unmask(post[j], h, t):
                x[j] = sample(post[j])   # reveal a real token, commit it
    t = t + h
return x                          # X_1, a generated sentence ~ p_data

The shape is identical to Lecture 1's flow-model sampler: draw the easy noise, loop n steps, return the endpoint. The differences are all "discrete": the noise is the all-mask sequence (not a Gaussian); each step un-masks (jumps) instead of drifting; and the network outputs posteriors over the vocabulary instead of a velocity vector.

The Gabriel García Márquez demo (from the slides). The lecture shows a real MDLM reconstructing the famous opening of One Hundred Years of Solitude. At t = 0 the passage is all dashes (fully masked). By t = 0.3 a few scattered words appear ("squad", "water", "polished"). By t = 0.6 phrases coalesce. By t = 0.8 nearly the whole paragraph is legible with a few holes. At t = 1.0 the full sentence stands: "Many years later, as he faced the firing squad, Colonel Aureliano Buendía was to remember that distant afternoon…" — revealed not left-to-right but as a field of words snapping into place.

Diffusion LM vs. autoregressive LM — the real tradeoff

 Autoregressive (GPT)Diffusion LM (MDLM)
OrderStrict left-to-rightAny order; often confidence-first
Per stepOne tokenMany tokens in parallel
Editing / in-fillHard (no native in-fill)Native — just mask & re-fill holes
KV cachingYes — reuse past keys/valuesNo — full sequence re-encoded each step
Inductive biasLeft-to-right matches causal textMust learn any-order generation (harder)
Is it worth it? An honest accounting (from the slides' discussion). Advantages: generate multiple tokens per step (potential speed), generate in any order (native editing/in-filling), and freedom to design new probability paths that may make more semantic sense than left-to-right. Disadvantages: no KV caching (each step re-encodes the whole sequence, so "fewer steps" doesn't automatically mean "faster"), and the model must learn to generate in any order, which is a harder learning problem than the natural left-to-right order of language. The jury is still out — but recent models (e.g. LLaDA at 100B scale) show the recipe genuinely competes with autoregressive LLMs.

Now build the full reverse sampler: iteratively un-mask, most-confident-first, committing each prediction until the exact target is reconstructed.

A diffusion LM can un-mask many tokens per step, so it should always be faster than an autoregressive LM. What's the catch?

Chapter 8: Showcase — A Masked-Diffusion Toy, End to End

Time for the payoff. Below is a complete masked-diffusion sampler running on a short target "sentence." It starts fully masked and, step by step, un-masks the most-confident positions until the whole sequence is reconstructed — exactly the loop of Chapter 7, animated. Play with the controls and watch how the number of steps and the per-step reveal budget trade off, and how the order is emergent (not left-to-right).

A diffusion language model reconstructing a sentence

The target is a short word sequence. Press Generate to run the reverse process: every step, the toy denoiser scores each masked slot's true word, and the most-confident slots reveal (teal) and stay committed. Bars under each token show the denoiser's confidence. Drag steps (more steps = more gradual, smoother refinement) and reveal budget (how many positions can un-mask per step). Notice the holes fill out of order, high-confidence words first — the hallmark of parallel, any-order diffusion decoding.

steps5
reveal budget2
What to notice. (1) The first step reveals the words the denoiser is surest about, wherever they sit — not necessarily the leftmost. (2) Once a word is revealed it never re-masks (absorbing commitment). (3) With a tiny step budget the model gambles, revealing many words at once on thin context; with more steps it eases in, using each freshly-revealed word as context for the next pass. (4) The end state is always the full target — the toy denoiser is "perfect," so this isolates the sampling dynamics from learning quality.

From toy to real. Swap the hand-coded denoiser for a transformer trained with the Discrete Flow Matching loss (Chapter 5), the 7-symbol alphabet for 50,000 word-pieces, and d = 7 for d = 1,024 — and you have a diffusion language model. The loop, the masking, the parallel confidence-ordered un-masking are unchanged. That is the entire lesson: the structure scales; only the denoiser and the dimensions grow.

(No quiz here — the simulation is the test. Make it converge in 2 steps, then in 9, and predict what changes before you drag.)

Chapter 9: Connections & Cheat Sheet

You rebuilt the entire flow-and-diffusion machine on discrete data — and discovered the recipe barely changed. Here is the whole lecture on one page, and where it sits in the series and the field.

The spine. Text is discrete, so there is no ODE/SDE — use a continuous-time Markov chain, where a rate matrix Q (rows sum to zero) plays the role of the vector field and the discrete Euler step δy=x + h·Qt(y|x) plays the role of the Euler step. The forward process masks tokens (factorized mixture path), fading data into all-[MASK] noise. The marginalization trick shows the marginal rate matrix is a reparameterization of a per-position denoiser p1|t(zj | x) — so training is just classification (cross-entropy), and sampling un-masks tokens in parallel, any-order.

Cheat sheet

State space. Vocabulary V, sequence length d ⇒ S = Vd (exponential — never enumerated). Extra token [MASK] marks erased positions.

CTMC. Random staircase trajectory: jumps between discrete states at random instants; memoryless (Markov). Discrete analogue of an ODE/SDE.

Rate matrix Q. Qt(y|x) ≥ 0 for y ≠ x (jump rate); diagonal Qt(x|x) = −(total outflow) so each row sums to zero (conserves probability). Factorized: only one-position jumps allowed ⇒ output shape d × V, linear not exponential.

Discrete Euler step. pt+h|t(y|x) ≈ δy=x + h·Qt(y|x). Sample the next state from this; the engine of the sampler.

Forward / masking path. Survival schedule κt (0→1). Each token survives w.p. κt, else → [MASK]. Expected masked count = d·(1−κt). Teleports mass (fade out true, fade in mask) — doesn't move it through space.

Denoising target. Marginal rate matrix ∝ p1|t(zj=vi|x): the posterior over the clean token per position. Generation = classification.

DFM loss. L = E[ ∑j −log pθ1|t(zj|x) ] — cross-entropy of the denoiser predicting the clean tokens of a corrupted sentence. Simulation-free.

Sampling. Start all-[MASK]; loop n steps: denoise → un-mask most-confident positions in parallel → commit → advance time. Returns X1 ∼ pdata.

The continuous lectures were the template

Where discrete diffusion is going

Related lessons on this site

The Feynman test. Open the Lecture 5 slides. Can you explain, from memory: why there is no SDE on text; what a CTMC is and how a rate matrix replaces the vector field; why rows of Q sum to zero; the masking forward process and its expected mask-count law; why the training target collapses to a per-position classifier; and how the reverse loop un-masks in parallel, any-order? And can you write the sampler in a dozen lines? If yes — you've finished the course.
One sentence: why does the flow matching recipe transfer so cleanly from continuous data to discrete text?