The LLM StackFrom Silicon to Agents
Part II — The Transformer Architecture
42 min read·Updated ·▶ Run the code (Colab)

2.12 Diffusion & Non-Autoregressive Language Models

Every model we have built so far in this book generates text the same way: one token at a time, strictly left to right, each token conditioned on all the tokens before it. This is the autoregressive (AR) factorization of the joint distribution over a sequence \(x = (x_1, \dots, x_L)\):

\[ p_\theta(x) = \prod_{t=1}^{L} p_\theta(x_t \mid x_{<t}) \]

It is a beautiful, exact factorization — and it is also the source of a stubborn latency wall. Generating a 1000-token response requires 1000 sequential forward passes, each of which must finish before the next can begin, because \(x_t\) is an input to the computation of \(x_{t+1}\). No amount of GPU parallelism removes that serial dependency. The hardware is busy but starved: at decode time you are matrix-vector limited, reading the entire model’s weights from memory to produce a single token (see The Anatomy of LLM Inference: Prefill, Decode & The KV Cache and Inference Economics: Latency, Throughput & Cost).

This chapter is about a different bet: non-autoregressive (NAR) language models, and in particular the discrete diffusion / masked diffusion family that has, since roughly 2024–2025, produced the first genuinely competitive non-AR LLMs — LLaDA, Dream, the commercial Mercury system from Inception Labs, and, in a signal that the idea has reached the frontier labs, Google DeepMind’s experimental Gemini Diffusion. Instead of emitting tokens left-to-right, these models start from a fully masked sequence and iteratively denoise all positions in parallel, refining the whole sequence over a small number of steps. The promise is a fundamentally different point on the latency–quality curve: tokens-per-second numbers that can be several times higher than AR models of comparable size, plus the ability to reason bidirectionally and fill text in any order.

We will develop the absorbing-state diffusion objective from first principles, contrast it sharply with next-token prediction (covered in The Pretraining Objective & Loss), implement a tiny masked-diffusion sampler from scratch, and then engineer our way toward the production tricks — semi-autoregressive block diffusion, KV-cache reuse, remasking schedules, and flexible-length generation — that make these systems practical. The mathematical machinery overlaps with continuous image diffusion (Diffusion Models & Generative Modeling (Breadth)), but the discrete-token setting has its own elegant simplifications that are worth seeing carefully.


Why Non-Autoregressive? The Latency Argument

Let us quantify the wall we are trying to climb over. In the decode phase, an AR transformer produces one token per forward pass. The wall-clock time to generate \(L\) tokens is

\[ T_{\text{AR}} \approx L \cdot t_{\text{step}} \]

where \(t_{\text{step}}\) is the per-step latency, dominated on modern GPUs by the time to stream the model weights from HBM into the compute units. For a dense model with \(P\) parameters at precision \(b\) bytes/param and memory bandwidth \(\beta\) bytes/s, the floor is roughly \(t_{\text{step}} \gtrsim P b / \beta\) — and crucially this floor is independent of how clever your kernel is, because decode is memory-bandwidth bound, not compute bound.

A non-autoregressive diffusion model instead runs \(N\) denoising steps, where each step is one forward pass over the entire length-\(L\) sequence in parallel, and ideally \(N \ll L\):

\[ T_{\text{NAR}} \approx N \cdot t_{\text{step}}' \]

Here \(t_{\text{step}}'\) is somewhat larger than \(t_{\text{step}}\) because each step processes all \(L\) positions at once (a compute-bound batched operation that uses the hardware far more efficiently), but the win is that \(N\) can be 16, 32, or 64 instead of \(L = 1000\). If a diffusion model produces acceptable text in \(N = 64\) steps for a 512-token output, it has done \(64\) sequential forward passes instead of \(512\) — an \(8\times\) reduction in serial depth, even before accounting for the fact that each diffusion forward pass keeps the matrix multiply units saturated.

Worked example: serial depth and tokens/sec

Suppose a 7B-parameter model in bf16 (\(b = 2\) bytes) runs on a GPU with \(\beta \approx 3.0\) TB/s of HBM bandwidth. The decode-step memory floor is

\[ t_{\text{step}} \gtrsim \frac{P \cdot b}{\beta} = \frac{7 \times 10^9 \times 2}{3.0 \times 10^{12}} \approx 4.7\ \text{ms}. \]
  • Autoregressive, 512 output tokens: \(T \approx 512 \times 4.7\ \text{ms} \approx 2.4\ \text{s}\), i.e. about 213 tokens/s for a single sequence.
  • Diffusion, 512 tokens in \(N = 32\) steps: each step reads the weights once and processes all 512 positions, so it is compute-heavier; say \(t_{\text{step}}' \approx 12\ \text{ms}\). Then \(T \approx 32 \times 12\ \text{ms} \approx 0.38\ \text{s}\) — about 1340 tokens/s for the same single sequence.

The diffusion model is ~6× faster for one sequence here, precisely because its serial depth (32) is far below the output length (512). The catch, which we will keep returning to, is whether 32 steps is enough to hit the quality the AR model gets “for free” by conditioning each token on exact predecessors. These magnitudes are illustrative; real numbers depend heavily on batch size, where AR throughput catches up because continuous batching (Continuous Batching & Request Scheduling) fills the compute units across many concurrent requests.

There is a second, qualitatively different motivation. AR models are causally blind to the future: when predicting \(x_t\) they cannot see \(x_{>t}\). This makes some tasks awkward — infilling, editing, satisfying global constraints, and the famous reversal curse (a model trained on “A is B” often fails to answer “what is B? → A”). A bidirectional denoiser conditions every position on every other position at every step, which structurally sidesteps these issues. Non-AR models are not just “AR but faster”; they have a different inductive bias.

Same output length (L = 8 tokens): what differs is SERIAL DEPTH Autoregressive -- one token per forward pass forward pass (memory-bound) 1 2 3 4 5 6 7 8 each new token conditions on the growing left context -- a fresh forward pass every step KV cache: +1 col/step 1 2 3 4 5 6 7 8 serial depth = L = 8 sequential passes Masked diffusion -- all positions per forward pass forward pass (whole row: 8x data) compute-heavier/step 1 M 2 M 3 M 4 M 5 M 6 M 7 M 8 M 1 2 3 serial depth = N = 3 passes, N << L Same output length; fewer DEPENDENT steps -> lower latency. Latency ~ serial depth, not total FLOPs -- diffusion trades heavier per-step compute for far fewer sequential steps. AR committed token diffusion [MASK] (unresolved) diffusion token (resolved) serial-depth step (counter)
Serial depth, not total compute, is what separates the two lanes. Filling the same 8-token output takes an autoregressive model 8 dependent forward passes (one token each) but a masked-diffusion model only 3 (each pass resolves several positions at once, at a heavier per-step compute cost). Latency tracks the number of dependent steps, so cutting serial depth from L to N << L is what makes diffusion decoding faster, even though it does more total FLOPs.

Discrete & Masked Diffusion From First Principles

Continuous diffusion (DDPM, score-based models) gradually adds Gaussian noise to a real-valued vector and learns to reverse it. Text is discrete — there is no meaningful “add a little Gaussian noise to the token cat.” We need a corruption process that lives on a finite vocabulary. The family that has proven both simple and powerful is absorbing-state (masked) diffusion.

The forward (corruption) process

Fix a special absorbing token [MASK], with index \(m\), that is not a normal vocabulary token. The forward process is defined on a continuous time variable \(t \in [0, 1]\). At \(t = 0\) the sequence is clean data; at \(t = 1\) it is entirely [MASK]. For each token independently, define a monotone masking schedule \(\alpha_t\) with \(\alpha_0 = 1\) and \(\alpha_1 = 0\). Conditioned on the clean token \(x_0^i\) at position \(i\), the corrupted token at time \(t\) is

\[ q(x_t^i \mid x_0^i) = \begin{cases} \alpha_t & \text{keep the original token } x_0^i,\\[4pt] 1 - \alpha_t & \text{replace with } \texttt{[MASK]}. \end{cases} \]

So \(\alpha_t\) is literally the probability that a given token survives unmasked at time \(t\). The absorbing property is what makes this clean: once a token becomes [MASK] it stays [MASK] for all later times — the only transition is data \(\to\) mask, never mask \(\to\) data and never data \(\to\) different-data. This is far simpler than the general discrete-diffusion transition matrices of Austin et al.’s D3PM, where any token can flip to any other.

The reverse (denoising) process and the objective

The model is a bidirectional transformer (an encoder-style stack with no causal mask — see Architecture Variants: Encoder-Decoder, Decoder-Only & Prefix-LM) that takes a partially-masked sequence \(x_t\) and predicts a distribution over the clean token at every masked position simultaneously. Call this \(p_\theta(x_0^i \mid x_t)\).

The remarkable simplification of absorbing diffusion is that, after the full variational derivation, the training loss collapses to a weighted cross-entropy on the masked positions only — essentially a continuous-time generalization of BERT’s masked language modeling. For a clean sequence \(x_0\), sample a time \(t \sim \mathcal{U}(0,1)\), mask each token independently with probability \(1 - \alpha_t\) to get \(x_t\), and minimize

\[ \mathcal{L}(\theta) = \mathbb{E}_{t \sim \mathcal{U}(0,1)} \; \mathbb{E}_{x_t \sim q(\cdot \mid x_0)} \left[ \frac{1}{1 - \alpha_t} \sum_{i : x_t^i = \texttt{[MASK]}} -\log p_\theta\!\left(x_0^i \mid x_t\right) \right]. \]

Read this carefully, because three ideas are packed into it:

  1. Only masked positions contribute. The sum runs over \(i\) where \(x_t^i\) is [MASK]. Unmasked positions provide context but no loss — the model is rewarded only for recovering what was hidden.
  2. The weight \(1/(1-\alpha_t)\) corrects for the masking rate. When \(t\) is near 1, almost everything is masked, \(1-\alpha_t \approx 1\), and the model must reconstruct from almost nothing — a hard denoising problem. When \(t\) is near 0, only a few tokens are masked and the weight is large, so each rare masked position counts heavily. This weighting is exactly what makes the masked-LM loss a valid (upper bound on the) negative log-likelihood, i.e. a true generative objective, not just a representation-learning trick. The MDLM and RADD analyses (Sahoo et al.; Ou et al., 2024) show this weighted form is a tight Evidence Lower Bound. (Strictly, the continuous-time ELBO carries the weight \(-\alpha_t'/(1-\alpha_t)\), where \(\alpha_t' = \mathrm{d}\alpha_t/\mathrm{d}t\). With the standard linear schedule \(\alpha_t = 1 - t\) we get \(\alpha_t' = -1\) and the weight collapses to \(1/(1-\alpha_t) = 1/t\), the form we use throughout this chapter. A pleasant result of the MDLM/Shi et al. analyses is that the ELBO’s value is invariant to the choice of monotone schedule — unlike continuous diffusion, where the noise schedule is a hyperparameter you tune — so the schedule only affects gradient variance, not the objective being optimized.)
  3. There is no left-to-right ordering. Unlike next-token prediction, where the chain rule dictates the factorization order, here the model learns to fill any subset of positions given any other subset. This is the source of the bidirectional advantage.

Contrast with next-token prediction, where the loss is \(-\sum_t \log p_\theta(x_t \mid x_{<t})\) over every position with a strict causal mask. AR predicts the next token from a left context; masked diffusion predicts a random subset from a bidirectional context. AR gives you an exact autoregressive likelihood and trivially correct sampling order; diffusion gives you parallelism and bidirectionality at the cost of an approximate, order-agnostic factorization.

Aside: why this is not just BERT

BERT also predicts masked tokens bidirectionally, so why can’t we just sample from BERT? BERT masks a fixed small fraction (~15%) of tokens and is never trained to generate from a fully-masked sequence, nor to handle the high-masking-rate regime. It also lacks the time-conditioning and the \(1/(1-\alpha_t)\) weighting that turn the masked-LM loss into a proper likelihood bound across all masking rates. Masked diffusion is “BERT trained at every masking rate from 0% to 100%, with the correct loss weighting, then sampled iteratively.” That generalization is exactly what lets it generate coherent long text, which BERT cannot.

Absorbing (mask) forward process, and the masked-only training objective

schedule alpha_0 = 1 alpha_1 = 0 t: 0 -> 1 alpha_t = P(token survives unmasked at time t)

t = 0 – clean data the cat sat on the mat

forward corruption: mask each token independently, P(mask) = 1 - alpha_t

t (mid-schedule) – partially masked the

[M]

sat

on

[M]

mat

ctx loss ctx ctx loss ctx grey = context only, no loss – accent = masked, contributes to loss

bidirectional transformer no causal mask reads the WHOLE row; predicts clean tokens ONLY at [MASK] positions p(x0^i | x_t)

as t -> 1, more tokens get masked

t = 1 – fully masked [M] [M] [M] [M] [M] [M]

training objective: masked-only, mask-rate-weighted cross-entropy

L = 1 / (1 - alpha_t) the weight x sum over i where x_t^i = [MASK] of -log p(x0^i | x_t) (sum runs ONLY over currently-masked positions i)

t -> 0: few masks weight is LARGE – each rare masked token counts heavily

t -> 1: many masks weight -> 1 – reconstruct from almost nothing

no left-to-right order: any subset of positions is recovered from any other subset as context.

Masked diffusion trains a BERT-style predictor at every masking rate. The forward process masks tokens independently with probability 1 - alpha_t, sweeping from clean data (t=0) to fully masked (t=1); a bidirectional transformer with no causal mask reads a partially masked row and predicts the clean token at each masked position. Only masked positions contribute to the loss, and the 1/(1-alpha_t) weight turns this into a valid likelihood bound rather than just a masked-LM heuristic.

Time conditioning, or the lack of it

Continuous diffusion models almost always feed the timestep \(t\) into the network (via sinusoidal embeddings added to the input). A pleasant surprise in the discrete-masked setting is that \(t\) is largely redundant: the number of [MASK] tokens in the input already tells the model roughly where it is in the denoising process. Several strong masked-diffusion LLMs (including LLaDA) drop explicit time conditioning entirely and rely on the mask count as an implicit clock. This is one fewer thing to get right and lets the architecture stay a vanilla bidirectional transformer.

The training step, end to end

The objective above is short enough to implement in a dozen lines, and — this is the practically important point — it bolts onto the GPT you already built. Everything from Building a GPT From Scratch (nanoGPT-style) and the Stack-100M block in The Stack-100M Architecture carries over unchanged: RoPE, RMSNorm, SwiGLU, GQA, the tokenizer, the packed uint16 shards, the optimizer, the WSD schedule. Exactly three things change.

  1. Delete the causal mask. F.scaled_dot_product_attention(q, k, v, is_causal=False) with no additive mask. That single flag converts the decoder into a bidirectional denoiser.
  2. Add one vocabulary entry for [MASK] — one extra row in the token embedding and in lm_head (a tied-embedding model gets both for free). Real checkpoints do this: LLaDA reserves a dedicated mask id rather than reusing an existing special token.
  3. Change the loss, and drop the shift. This is the most common bug in a first implementation. An AR model’s logits at position \(i\) predict token \(i+1\), so training shifts targets by one. A diffusion denoiser predicts the token at position \(i\) from the corrupted sequence — no shift. If you copy an AR training loop verbatim you get a model that trains to a plausible-looking loss and generates garbage.
import torch
import torch.nn.functional as F

def masked_diffusion_loss_batch(model, x0, mask_id, eps=1e-3):
    """
    Batched absorbing-diffusion training loss (the LLaDA/MDLM form).

    model: bidirectional transformer, (B, L) ids -> (B, L, V) logits. NO causal mask.
    x0:    (B, L) clean token ids from your ordinary packed pretraining shards.
    Linear schedule alpha_t = 1 - t, so P(mask) = t and the ELBO weight is 1/t.
    Returns a scalar ready for .backward().
    """
    B, L = x0.shape
    # One t PER SEQUENCE, not per token: the forward process draws a single noise
    # level for the sequence and then masks its tokens i.i.d. at that level.
    # Clamp t away from 0 so the 1/t weight cannot explode a single microbatch.
    t = torch.rand(B, 1, device=x0.device) * (1.0 - eps) + eps      # (B, 1)
    masked = torch.rand(B, L, device=x0.device) < t                 # broadcast -> (B, L)

    # A row with zero masked positions contributes no gradient; force one.
    # (Bias is negligible: P(no mask) = (1-t)^L is tiny for L in the thousands.)
    masked[:, 0] |= ~masked.any(dim=1)

    xt = torch.where(masked, torch.full_like(x0, mask_id), x0)
    logits = model(xt)                                              # (B, L, V)

    # Cross-entropy on MASKED positions only. NOTE: no shift -- logits[b, i]
    # predicts x0[b, i], unlike the next-token objective.
    ce = F.cross_entropy(logits[masked], x0[masked], reduction="none")   # (n_masked,)

    # Weight each masked position by 1/t of ITS OWN sequence, then normalize by
    # B*L (per-token normalization) so the loss is comparable across batches.
    w = t.expand(B, L)[masked]                                      # (n_masked,)
    return (ce / w).sum() / (B * L)

Two consequences worth internalizing. First, the number reported by this loss is not a perplexity you can compare to an AR run — it is a Monte-Carlo estimate of an ELBO, and its variance is high early in training because a single \(t\) per sequence is a coarse estimator (antithetic or low-discrepancy sampling of \(t\) across the batch is the standard variance reduction). Second, the diffusion model sees strictly less supervision per forward pass: an AR pass gets a gradient at all \(L\) positions, a diffusion pass only at the \(\approx tL\) masked ones (in expectation, half of them). Empirically this is a real part of why from-scratch masked-diffusion LLMs need more tokens or more epochs to match AR quality at equal parameters, and it is the single strongest argument for the AR-to-diffusion adaptation route that Dream took.

For SFT the recipe is a small variant: never mask the prompt. Concatenate prompt and response, corrupt only the response positions, and compute the loss only there — the prompt is permanently-clean context, exactly as is_prompt is clamped in the sampler below. That is the diffusion analogue of the prompt-masked SFT loss in Supervised Fine-Tuning & Instruction Tuning.

Concretely, this is a weekend project on top of the capstone: keep the corpus and packing of Data: Sourcing, Filtering, Dedup, Tokenize & Pack ~20B Tokens and the training loop of The Pretraining Run: A Complete Single-GPU Training Loop exactly as they are, set is_causal=False, grow the vocab by one, and swap the shifted cross-entropy for masked_diffusion_loss_batch. You get a ~100M-parameter masked-diffusion LM you can sample with the code in the next section — an ablation against the AR Stack-100M that costs one extra run and teaches more about the two paradigms than any benchmark table. Budget more tokens than the AR baseline for the reason just given, and compare on generation quality rather than on loss, since the two numbers measure different things.


Iterative Parallel Denoising: A Tiny Sampler

Time to make this concrete. The inference loop for masked diffusion is conceptually simple: start with everything masked, and repeatedly (a) predict all masked positions in parallel, (b) commit some of those predictions by unmasking them, and © leave the rest masked for the next round. The art is entirely in which positions to commit each step — the remasking schedule.

Here is a complete, from-scratch sampler. The “model” is abstracted behind a denoiser callable so the loop is crystal clear; in practice it is a bidirectional transformer returning logits of shape (L, V).

import torch
import torch.nn.functional as F

MASK_ID = 0  # reserve index 0 in the vocab for the [MASK] absorbing token

@torch.no_grad()
def masked_diffusion_sample(
    denoiser,            # callable: (LongTensor[L]) -> FloatTensor[L, V] logits
    L: int,              # sequence length to generate
    num_steps: int,      # number of denoising iterations N (the serial depth)
    vocab_size: int,
    temperature: float = 0.0,   # 0.0 = greedy/argmax per position
    prompt: torch.Tensor = None,   # optional LongTensor of clamped prefix tokens
    clamp_mask: torch.Tensor = None,    # optional (L,) bool mask of arbitrary clamped positions
    clamp_values: torch.Tensor = None,  # (L,) values at those positions; used with clamp_mask
    device: str = "cpu",
):
    """
    Absorbing-state masked diffusion sampler.

    Strategy ("confidence-based remasking", a la LLaDA's low-confidence remasking):
    at each step we predict ALL masked positions, but only *keep* (unmask) the
    most confident predictions, sized so that the fraction of masked tokens
    follows a linear schedule from 1.0 down to 0.0 over num_steps. Less confident
    positions are returned to [MASK] and revisited in later steps.
    """
    # 1. Start fully masked.
    x = torch.full((L,), MASK_ID, dtype=torch.long, device=device)

    # 2. Clamp fixed context (these positions are never masked and never predicted).
    #    `clamp_mask`/`clamp_values` clamp an ARBITRARY subset of positions (e.g. a
    #    prefix AND a suffix for infilling); `prompt` is the common special case of
    #    clamping only a contiguous prefix.
    is_prompt = torch.zeros(L, dtype=torch.bool, device=device)
    if clamp_mask is not None:
        is_prompt = clamp_mask.to(device)
        x[is_prompt] = clamp_values.to(device)[is_prompt]
    elif prompt is not None:
        x[: prompt.numel()] = prompt.to(device)
        is_prompt[: prompt.numel()] = True

    # 3. Denoising schedule: target number of STILL-masked tokens after step k.
    #    Linear from "all generatable positions masked" down to 0.
    n_gen = int((~is_prompt).sum().item())   # positions we actually generate
    # masked_target[k] = how many gen-positions remain masked AFTER step k+1
    masked_target = [
        round(n_gen * (1.0 - (k + 1) / num_steps)) for k in range(num_steps)
    ]

    for step in range(num_steps):
        masked = (x == MASK_ID) & (~is_prompt)   # positions still to fill
        if masked.sum() == 0:
            break

        logits = denoiser(x)                     # (L, V) — full bidirectional pass
        logits[:, MASK_ID] = -float("inf")       # never predict [MASK] itself

        if temperature > 0.0:
            probs = F.softmax(logits / temperature, dim=-1)
            pred = torch.multinomial(probs, num_samples=1).squeeze(-1)  # (L,)
            conf = probs.gather(-1, pred.unsqueeze(-1)).squeeze(-1)     # (L,)
        else:
            probs = F.softmax(logits, dim=-1)
            conf, pred = probs.max(dim=-1)       # greedy + its confidence

        # Candidate fill: tentatively set every masked position to its prediction.
        x_candidate = x.clone()
        x_candidate[masked] = pred[masked]

        # Decide how many of the currently-masked positions to KEEP unmasked.
        n_keep = int(masked.sum().item()) - masked_target[step]
        n_keep = max(0, n_keep)

        # Rank masked positions by confidence; keep the top-n_keep, remask the rest.
        conf_masked = conf.clone()
        conf_masked[~masked] = -float("inf")     # only compete among masked positions
        if n_keep > 0:
            keep_idx = torch.topk(conf_masked, n_keep).indices
            new_x = x.clone()
            new_x[masked] = MASK_ID              # provisionally remask all
            new_x[keep_idx] = x_candidate[keep_idx]  # commit the confident ones
            x = new_x
        # if n_keep == 0 we commit nothing this step (rare; only at the very start)

    # Final cleanup: fill any leftover masks greedily (last step should handle this).
    leftover = (x == MASK_ID) & (~is_prompt)
    if leftover.any():
        logits = denoiser(x)
        logits[:, MASK_ID] = -float("inf")
        x[leftover] = logits[leftover].argmax(dim=-1)

    return x

The whole behavior of the model lives in how n_keep is chosen each step. A few canonical schedules:

  • Random remasking. Pick the positions to keep uniformly at random. Simple, unbiased, but wastes steps committing to low-confidence guesses early.
  • Confidence-based (low-confidence remasking). Keep the highest-confidence predictions, return the rest to [MASK] (what the code above does). This is LLaDA’s default and works well because the model “locks in” the tokens it is sure about and keeps reconsidering the uncertain ones with more context each round.
  • Top-\(k\) / greedy decoding orders. Variants that commit a fixed number of tokens per step, or use entropy rather than max-prob as the confidence measure.

The crucial subtlety is that already-committed tokens become context for the next step. When the model unmasks “The capital of France is” it makes the later positions far easier to predict — the bidirectional pass now sees both left and right context flowing into each remaining mask. This is the iterative-refinement engine: each round of commitments sharpens the conditional distribution for everything still masked.

Common pitfall: the conditional-independence trap

In a single denoising step the model predicts every masked position independently — it factorizes \(p_\theta(x_0 \mid x_t) = \prod_i p_\theta(x_0^i \mid x_t)\). That independence is wrong for natural language: the joint over masked tokens is highly correlated. If you tried to fill all masks in one step, you would get incoherent output where each position is individually plausible but jointly contradictory (e.g. subject and verb disagreeing). Iterative denoising with remasking is precisely the fix: by committing only a few high-confidence tokens per step and re-conditioning, you recover the correlations the single-step factorization throws away. More steps trade compute for coherence. This is the diffusion-LM analog of why you can’t sample all pixels of an image at once.

How many steps do you actually need?

This is the central quality–latency knob. With \(N = L\) steps and a one-token-per-step schedule, masked diffusion essentially reduces to an (order-flexible) autoregressive model and matches AR quality — but you have thrown away the speed advantage. With \(N\) very small (say 8), you get blazing speed but the conditional-independence error bites and quality drops. Production systems live in the interesting middle: enough steps that each commits a small block of tokens, few enough that serial depth stays well below \(L\). The empirical finding across LLaDA and Dream is that you can often reach AR-comparable quality at \(N\) on the order of \(L/4\) to \(L/2\), and trade further quality for speed below that.

Iterative parallel denoising: commit confident, re-condition, repeat 8 positions start as [MASK]; bidirectional attention lets every committed position condition every prediction that follows 1 STEP 1 predict all 8 masked positions in parallel -- commit top 2 by confidence 6 / 8 masked the 0.91 [M] 0.55 [M] 0.38 [M] 0.62 [M] 0.58 [M] 0.31 [M] 0.29 mat 0.89 2 STEP 2 6 still masked, now conditioned on 2 committed -- commit 2 more 4 / 8 masked the cat 0.83 [M] 0.44 on 0.81 [M] 0.52 [M] 0.35 [M] 0.33 mat 3 STEP 3 4 still masked, now conditioned on 4 committed -- commit 2 more 2 / 8 masked the cat sat 0.88 on the 0.85 [M] 0.42 [M] 0.40 mat 4 STEP 4 2 still masked, now conditioned on 6 committed -- commit both, done 0 / 8 -- done the cat sat on the warm 0.72 soft 0.69 mat ONE-SHOT: fill all 8 positions at once each masked position predicted independently -- individually plausible, jointly incoherent the cat sit on the mat x subject/verb disagreement (cat .. sit) ITERATE: commit confident, re-condition committed tokens become context for the next step -- predictions sharpen as masks shrink the cat sat on the warm soft mat check: grammatical and coherent each committed token becomes context for the next step N steps recover the correlations one step throws away -- more steps trade compute for coherence
Confidence-based remasking recovers coherence one commit at a time. Each step predicts every still-masked position in parallel with a confidence score, but commits only the most confident two (locked, checked); the rest return to [MASK] and get re-predicted with more context next round -- notice the confidence scores on the remaining masked positions climb from step to step as more of the sentence locks in. Filling all 8 positions in a single shot leaves each token individually plausible but jointly incoherent, because the single-step factorization treats masked positions as independent -- iterating trades compute for the coherence that independence throws away.

Semi-Autoregressive Block Diffusion

Pure diffusion over a fixed-length block has two weaknesses that show up immediately in production: (1) it commits to a fixed sequence length up front, which is unnatural for open-ended generation, and (2) it cannot reuse a KV cache across steps, because every position is recomputed every step. Block diffusion (Arriola et al., 2025) is the hybrid that fixes both by interpolating between autoregressive and diffusion modeling.

The idea: split the sequence into contiguous blocks of \(B\) tokens each. Generate the blocks autoregressively (left to right, one block after another), but generate the tokens within each block by diffusion (parallel denoising). Formally, with blocks \(x^{(1)}, x^{(2)}, \dots\),

\[ p_\theta(x) = \prod_{b} p_\theta^{\text{diff}}\!\left(x^{(b)} \mid x^{(1)}, \dots, x^{(b-1)}\right), \]

where each block factor is itself a small masked-diffusion model conditioned on all previously finalized blocks. Set \(B = 1\) and you recover a pure autoregressive model; set \(B = L\) (one block) and you recover pure diffusion. Block diffusion is the dial between them.

   Block 1            Block 2            Block 3
 [denoise B tok] -> [denoise B tok] -> [denoise B tok] -> ...
   (parallel)         (parallel)         (parallel)
   |__________________|__________________|
        previous blocks are FINALIZED and cached in the KV cache;
        the current block attends to them causally (block-causal mask)

This buys three things at once:

  • KV-cache reuse. Because earlier blocks are finalized before the current block starts, their keys and values are fixed. We compute them once and cache them, exactly like AR decoding (Prefix Caching & KV-Cache Reuse, PagedAttention & KV-Cache Memory Management). Within-block denoising recomputes only the \(B\) positions of the current block — a huge saving versus pure diffusion, which recomputes all \(L\) positions every step.
  • Flexible / arbitrary length. Generation proceeds block by block until an end-of-sequence condition fires, so you are no longer locked into a length chosen before you start. You can keep emitting blocks until the model produces [EOS], just like AR.
  • Parallelism where it pays. Inside a block you get the diffusion speedup (parallel denoising of \(B\) tokens in a few steps); across blocks you get the AR structure that captures long-range left-to-right dependencies and gives you the cache.

The attention mask is the key implementation detail. Within a block, attention is bidirectional (every position sees every other position in the block — that is what gives diffusion its power). Across blocks, attention is causal at block granularity: tokens in block \(b\) can attend to all tokens of blocks \(1..b\) but not \(b+1..\). This block-causal mask is what makes the KV cache valid.

def block_causal_mask(L: int, block_size: int) -> torch.Tensor:
    """
    Build an additive attention mask for block diffusion.
      - bidirectional WITHIN a block (no causal restriction)
      - causal ACROSS blocks (block b sees blocks 1..b only)
    Returns (L, L) with 0.0 where attention is allowed, -inf where blocked.
    """
    idx = torch.arange(L)
    block_of = idx // block_size                       # (L,) block index per position
    # allowed if key's block <= query's block
    allowed = block_of[None, :] <= block_of[:, None]   # (L_query, L_key) boolean
    mask = torch.zeros(L, L)
    mask.masked_fill_(~allowed, float("-inf"))
    return mask

Training block diffusion has one wrinkle the sampler hides. Each block must be denoised while attending to the clean (finalized) versions of all earlier blocks, but the earlier blocks in your training batch are themselves noised. BD3-LM’s solution is a vectorized two-forward-pass scheme: one pass over the clean sequence under the block-causal mask to materialize keys and values for every block, then a second pass over the noised sequence in which each block’s queries attend to those cached clean keys/values for blocks \(<b\) and to its own noised block bidirectionally. That keeps training \(O(1)\) passes per batch instead of one pass per block. The paper also reports that the naive \(t \sim \mathcal{U}(0,1)\) draw has high gradient variance at block granularity and uses clipped noise schedules — sampling \(t\) from a narrower interval \([\beta, \omega] \subset (0,1)\) tuned per block size — to reduce it, which is the main reason BD3-LM reaches better perplexities than earlier discrete-diffusion models.

Block diffusion is the architecture that most directly maps onto the existing high-performance AR serving stack: you keep continuous batching, paged KV cache, and prefix caching, and you simply swap the inner decode kernel from “one token per step” to “a block of tokens per few diffusion steps.” This is a large part of why the first commercially viable diffusion LLM, Mercury, is built on this semi-autoregressive structure rather than pure parallel diffusion.

Practitioner tip: block size is your latency–quality dial

In a block-diffusion deployment you have two coupled knobs: block size \(B\) and per-block denoising steps \(N_B\). Small \(B\) with few steps behaves like AR (high quality, lower parallelism). Large \(B\) exposes more parallelism per block but raises the conditional-independence burden, demanding more denoising steps to stay coherent. A common sweet spot is a moderate block (e.g. 16–32 tokens) with a handful of denoising steps per block, so that each block commits a few tokens at a time while the cache amortizes the cost of all earlier blocks. Tune \(B\) and \(N_B\) jointly against your target time-to-first-token and inter-token latency.

Semi-autoregressive block diffusion: causal across blocks, parallel within a block

Block 1 (finalized) 1 2 3 4 KV cache: fixed computed once, reused

Block 2 (finalized) 5 6 7 8 KV cache: fixed computed once, reused

Block 3 (current) 9

10

[M]

[M] parallel denoise N_B diffusion steps

autoregressive across blocks block b attends to all of blocks 1..b – previous blocks are finalized and cached

block-causal attention mask (3 blocks x 4 tokens) within-block (bidirectional) earlier block (causal, cached) future block (blocked)

block size B is the latency/quality dial typical: B ~ 16-32 B = 1 pure autoregressive B = L pure diffusion

flexible length keep emitting blocks until [EOS] – no fixed length up front

cache reuse only the current block is recomputed each step

Block diffusion interpolates between autoregressive and diffusion by choosing a block size B. Finalized blocks (1, 2) have a fixed KV cache computed once and reused; the current block (3) is denoised in parallel with full bidirectional attention inside it, while attention across blocks stays causal -- a block sees all earlier blocks but never a future one. B = 1 recovers pure autoregressive decoding; B = L recovers pure diffusion; production systems sit in between to get cache reuse, flexible-length generation, and within-block parallelism at once.

Production Systems & Inference Economics

We now have the pieces — masked-diffusion objective, iterative denoising, block structure, KV reuse — to understand the real systems and their economics.

The landscape

  • LLaDA (Nie et al., 2025) is the proof of concept at scale: an 8B-parameter masked-diffusion LLM trained from scratch with the absorbing objective, with an instruction-tuned chat variant. Its headline result is that a pure (non-block) masked-diffusion model can be competitive with similarly-sized autoregressive LLaMA-class models on standard benchmarks — the first time a from-scratch diffusion LLM closed most of that gap. It also concretely demonstrated the bidirectional/reversal advantage (below).
  • Dream (2025) is a 7B masked-diffusion LLM that, rather than training from scratch, initializes from an existing autoregressive checkpoint (a Qwen2.5-7B base model) and adapts it to the diffusion objective — a clever way to reuse the enormous compute already spent on AR pretraining, at a small fraction of from-scratch cost. It uses context-adaptive token-level noise and confidence-based decoding orders to push quality. This adaptation route is the one to copy if you want a diffusion LM without a pretraining budget: take any open base model, drop the causal mask, add a mask token, and continue training under the loss above.
  • Mercury (Inception Labs, 2025) is the commercial diffusion-LLM system, marketed around coding (Mercury Coder) and built on a block-diffusion-style architecture for flexible length and KV reuse. Its technical report puts Mercury Coder Mini and Small at roughly 1109 and 737 tokens/sec on an NVIDIA H100 — up to about 10× the throughput of comparable speed-optimized autoregressive code models at similar quality — by exploiting parallel decode. (The mechanism — parallel denoising of blocks with cache reuse — is the durable takeaway; treat exact numbers as hardware- and configuration-dependent.)
  • Gemini Diffusion (Google DeepMind, 2025) and Seed Diffusion (ByteDance, 2025) are the clearest signal that the approach has reached the frontier and the speed frontier respectively: the former is an experimental text-diffusion model reported at on the order of 1,400 tokens/sec while matching Google’s fast autoregressive models on coding and math benchmarks; the latter is a code-focused discrete-diffusion model reporting above 2,000 tokens/sec on datacenter GPUs. Their existence — more than any single vendor figure — is the evidence that parallel-decode diffusion is now a mainstream research direction rather than an academic curiosity.

The open-source stack: what you actually run

The from-scratch code above is the mechanism; here is the tooling that implements it. Open-weight diffusion LLMs ship on the HuggingFace Hub with custom modeling code, so they load through transformers with trust_remote_code=True — but note that model.generate() in its usual AR form does not apply, because the generation loop is a denoiser, not a next-token loop. The repos therefore ship their own generate function implementing the remasking schedule.

# Loading a released masked-diffusion LLM through HuggingFace transformers.
# Repo ids move; confirm the exact one on the Hub before depending on it.
import torch
from transformers import AutoModel, AutoTokenizer

repo = "GSAI-ML/LLaDA-8B-Instruct"          # LLaDA's instruction-tuned release
tok = AutoTokenizer.from_pretrained(repo, trust_remote_code=True)
model = AutoModel.from_pretrained(
    repo, trust_remote_code=True, torch_dtype=torch.bfloat16
).to("cuda").eval()

# The absorbing token is a REAL vocabulary entry in released checkpoints -- read
# it from the config/tokenizer instead of hardcoding index 0 as our toy sampler did.
mask_id = getattr(model.config, "mask_token_id", None) or tok.mask_token_id

# From here the loop is the one we wrote by hand: start from a fully-[MASK]ed
# answer block after the prompt, forward the whole sequence, commit the most
# confident positions, repeat. The reference `generate.py` in the model's repo
# implements exactly this, plus semi-autoregressive block decoding.

The pieces of the ecosystem worth knowing by name:

  • Reference training code. The Kuleshov group’s mdlm and bd3lms repositories are the canonical implementations of the simplified masked-diffusion objective and of block diffusion respectively — Hydra-configured PyTorch Lightning training on OpenWebText/LM1B, and the cleanest place to read a correct ELBO implementation. The LLaDA authors’ repo (ML-GSAI/LLaDA) is the reference for the 8B-scale sampler, including low-confidence remasking and semi-autoregressive block decoding.
  • Inference acceleration. Fast-dLLM (NVIDIA, 2025) is the notable training-free accelerator: it approximates a KV cache for block-wise bidirectional attention (caching prefix and suffix blocks) and adds confidence-thresholded parallel decoding — commit every position whose probability exceeds a threshold in one shot, rather than a fixed count — reporting large end-to-end speedups on LLaDA and Dream with minimal quality loss. The mechanism is the durable part: adaptive commit counts beat the fixed linear schedule our toy sampler uses.
  • Serving stacks. The mature engines (vLLM: Architecture, PagedAttention & Internals, SGLang: RadixAttention & Structured Programs, TensorRT-LLM, TGI & Other Serving Stacks) are built around the AR decode loop — a per-request single-token step, paged KV cache, continuous batching. First-class diffusion-LM support is emerging rather than settled; check the current release notes rather than assuming parity. In practice today you serve a diffusion LLM from the model repo’s own loop wrapped in torch.compile and CUDA graphs (Kernel Fusion, torch.compile, CUDA Graphs & Compilers), and you get continuous batching only if you write it. Quantization (GPTQ/AWQ/bitsandbytes) applies unchanged, since it operates on the weights and is indifferent to the decode loop.
  • Evaluation. lm-evaluation-harness (Building Eval Harnesses) mostly works, with one caveat that matters: its loglikelihood request type assumes an exact AR log-probability. A diffusion model can only supply a Monte-Carlo ELBO estimate, so multiple-choice tasks scored by log-likelihood are noisy and not strictly comparable to AR numbers. Generative tasks (generate_until) are the honest comparison; wire the model’s denoising generate into a custom LM subclass.

Where the speed actually comes from — and where it doesn’t

The diffusion speedup is a serial-depth win, not a FLOPs win. A diffusion model often does more total floating-point work than an AR model (it recomputes positions across steps), but it organizes that work into fewer sequential, more parallel steps. This has two important consequences for the economics (Inference Economics: Latency, Throughput & Cost):

  1. Single-stream latency: diffusion wins big. When you have one request and want the answer fast — interactive coding, low-batch agentic loops — cutting serial depth from \(L\) to \(N\) is a direct latency reduction. This is diffusion’s home turf.
  2. High-throughput, large-batch serving: the gap narrows. AR decoding is memory-bandwidth bound at small batch but becomes compute-efficient at large batch, because continuous batching packs many requests’ single-token steps into one fat matrix multiply that saturates the GPU. Diffusion already saturates the GPU per step, so it has less headroom to gain from batching. At very high concurrency, an AR system’s aggregate tokens/sec can rival or exceed a diffusion system’s, because the diffusion model’s extra FLOPs per token now compete for the same saturated compute. Diffusion’s advantage is largest at low-to-moderate batch and long single outputs.

The honest summary: diffusion LLMs are not a free lunch that beats AR everywhere. They occupy a different region of the latency–throughput–quality surface — superb single-stream latency and bidirectional capability, in exchange for an approximate factorization, more total compute per token, and (today) a smaller ecosystem of training data, tooling, and post-training recipes than the mature AR stack.

Worked example: the FLOPs–latency trade in block diffusion

Take a block-diffusion model, \(L = 512\) output tokens, block size \(B = 32\) (so 16 blocks), and \(N_B = 8\) denoising steps per block. Count forward passes over a block’s worth of positions:

  • Total block-denoising passes \(= 16 \text{ blocks} \times 8 \text{ steps} = 128\) passes, each over 32 positions.
  • The equivalent AR model needs \(512\) sequential single-token passes.

Serial depth drops from 512 → 128 (a 4× reduction in the number of dependent steps), which is roughly the single-stream latency win. But total positions processed by the denoiser \(\approx 128 \times 32 = 4096\) versus AR’s \(512\) — about 8× more position-evaluations. With KV-cache reuse the earlier blocks aren’t recomputed, so the attention cost is bounded, but the within-block MLP work is genuinely ~8× larger. That extra compute is the price of the parallelism: cheap when the GPU was idle (low batch), expensive when it was already full (high batch). This is the whole economic story in one example.

Sampling controls carry over — mostly

Temperature, top-\(k\), and top-\(p\) (Sampling Strategies & Decoding Algorithms) all apply per position inside a denoising step, exactly as in AR. What is new is the remasking schedule as a first-class decoding hyperparameter: how many tokens to commit per step, and by what confidence criterion. Constrained/structured generation (Structured & Constrained Generation) is actually more natural in some respects — because the model sees the whole sequence, you can pin known tokens (a closing brace, a required JSON key) as permanently-unmasked context and let denoising fill around them, which is exactly the infilling capability AR models lack.


The Bidirectional Advantage: Infilling, Constraints & the Reversal Curse

The most intellectually interesting reason to care about non-AR LLMs is not speed — it is bidirectionality. Because a masked-diffusion model conditions every position on every other position, it can do things that are structurally hard for a left-to-right model.

Bidirectionality in practice: native infilling, and robustness to the reversal curse Infilling: denoise a hole conditioned on both sides Diffusion The cat [M] [M] the mat prefix (clamped) suffix (clamped) hole conditioned on both sides AR (for comparison) The cat ? ? the mat AR: the future to the right is unavailable (faded) -- must bolt on fill-in-the-middle The reversal curse: training direction determines what can be recalled Training pair: "A is B" A B AR A B gradient flows A -> B only, during training query: "what is B?" ? stumbles -- no reverse gradient ever seen Diffusion A B either side maskable -> reconstructs A given B, and B given A query: "what is B?" A resolves correctly -- both directions trained
Bidirectionality is a structural capability, not just a speed trick. Diffusion fills a masked hole by conditioning on prefix and suffix simultaneously (native infilling), while an autoregressive model only ever sees the left context and must bolt on fill-in-the-middle. The same bidirectional training -- reconstructing either side from the other -- is why diffusion LMs resist the reversal curse that trips up AR models trained only on the forward "A is B" gradient.

Infilling and editing. Given a document with a hole in the middle, an AR model must either be specially trained with fill-in-the-middle objectives or awkwardly re-prompted. A diffusion model treats infilling as its native operation: clamp the known prefix and suffix as unmasked context, mask the hole, and denoise. The hole is filled conditioned on both sides, which is exactly what coherent editing requires.

Global constraints. Tasks like “write a sentence that ends with this exact word” or “produce code with this signature and this return statement” require coordinating the beginning and end of the output. Left-to-right generation can paint itself into a corner; bidirectional denoising can place the constrained tokens first and grow the rest around them.

The reversal curse. AR models trained on “A is B” famously struggle to answer the reversed query “who/what is B?” because the training gradient only ever flowed in the A→B direction. A bidirectional diffusion model is trained to reconstruct any masked subset from any context, so it sees both directions during training and is markedly more robust to reversal. LLaDA’s authors highlighted exactly this: on reversal-style tasks the masked-diffusion model can outperform a same-scale autoregressive baseline, sometimes by a wide margin, precisely because its objective is not directionally biased.

# Infilling with the same sampler: clamp prefix AND suffix, denoise the middle.
def infill_example(denoiser, prefix_ids, suffix_ids, hole_len, vocab_size):
    L = len(prefix_ids) + hole_len + len(suffix_ids)
    x = torch.full((L,), MASK_ID, dtype=torch.long)
    is_clamped = torch.zeros(L, dtype=torch.bool)

    # left context
    x[: len(prefix_ids)] = torch.tensor(prefix_ids)
    is_clamped[: len(prefix_ids)] = True
    # right context (note: this is the future, which AR cannot use!)
    x[len(prefix_ids) + hole_len :] = torch.tensor(suffix_ids)
    is_clamped[len(prefix_ids) + hole_len :] = True

    # Reuse the diffusion loop, but treat ALL clamped positions (prefix AND
    # suffix) like a "prompt" so they are never masked and never overwritten.
    # The hole is denoised conditioned on BOTH sides simultaneously.
    return masked_diffusion_sample(
        denoiser, L=L, num_steps=hole_len, vocab_size=vocab_size,
        clamp_mask=is_clamped, clamp_values=x,
    ), is_clamped

This is the capability that AR models have to bolt on and diffusion models get for free, and it is the strongest argument that non-AR LMs are a genuinely different tool rather than a faster clone.

Interview Corner

Q: A masked-diffusion LM predicts all masked positions in parallel within a single denoising step. If that step models the masked tokens as conditionally independent given the context, how can the final generated text be coherent — and what determines the quality/speed trade-off?

A: The single-step conditional-independence assumption is wrong for language — the joint over masked tokens is strongly correlated — but the sampler never relies on filling everything in one step. It commits only a subset of high-confidence predictions per step (confidence-based remasking) and re-conditions the next step’s predictions on those newly committed tokens. Iterating \(N\) steps recovers the inter-token correlations that any single step’s factorization discards, because each committed token becomes bidirectional context that sharpens the conditionals for the still-masked positions. The trade-off is set by \(N\): at \(N = L\) (one token per step) it degenerates to an order-flexible autoregressive model and matches AR coherence but loses the speed; at small \(N\) each step commits many tokens, the independence error grows, and quality drops. Production systems pick the smallest \(N\) (often roughly \(L/4\) to \(L/2\), or a few steps per block in block diffusion) that holds quality, which is where the tokens/sec win comes from — it’s a reduction in serial depth, not in total FLOPs. The key insight to land is that the win is parallelism in the sampling order, paid for with extra compute and an approximate factorization, and that bidirectionality is a separate, structural benefit (infilling, reversal-robustness) independent of the speed argument.


Practicalities, Limits & When to Reach for This

A few engineering realities to keep the picture honest:

  • Likelihood is a bound, not exact. AR models give you an exact log-likelihood (useful for perplexity, ranking, watermarking). Masked diffusion gives a variational bound; reported perplexities use the ELBO and are not directly comparable to AR perplexity. Be careful when comparing benchmark tables across the two paradigms.
  • Post-training is younger. The instruction-tuning, RLHF, and preference-optimization stack (Part V) was built around AR generation. SFT ports cleanly (mask only the response, as above). The hard part is RL and preference optimization, because PPO/GRPO/DPO all need a per-sequence log-probability \(\log \pi_\theta(y \mid x)\) and a well-defined generation order (Policy Gradients & PPO for Language Models, GRPO, RLOO & Critic-Free RL, Direct Preference Optimization & Its Variants) — and a diffusion LM has neither. The workaround in the literature is to substitute the ELBO estimate for the exact log-prob: d1 (2025) introduces diffu-GRPO, a GRPO variant that uses a one-forward-pass, per-token ELBO surrogate for the policy log-prob so that RLVR (RL with Verifiable Rewards (RLVR) & The Reasoning Recipe) can run on a masked-diffusion LLM; LLaDA 1.5 attacks the same problem on the preference side with variance-reduced preference optimization (VRPO), whose whole content is reducing the variance of that Monte-Carlo ELBO so DPO-style gradients are not swamped by noise. Both are the same lesson: the missing exact likelihood is the central obstacle to diffusion post-training, and every method here is a variance-reduction story around estimating it.
  • Length handling. Pure diffusion needs a length up front; block diffusion fixes this but adds the block-size hyperparameter and a block-causal mask. Either way, length control is more involved than AR’s natural “generate until [EOS].”
  • Tooling maturity. vLLM, SGLang, speculative decoding (Speculative Decoding: Draft Models, Medusa, EAGLE & Lookahead), and the entire kernel ecosystem are tuned for AR decode. Diffusion-specific serving is catching up but is not yet at parity.

So when should you reach for a diffusion LLM today? When single-stream latency dominates your objective (interactive coding assistants, low-concurrency agents), when the task is natively bidirectional (infilling, editing, constraint satisfaction), or when you want robustness to reversal-style queries. When you need exact likelihoods, the mature post-training/RL stack, maximal throughput at very high batch, or simply the lowest-risk path, autoregressive transformers remain the default — for now.

Diffusion and non-AR language models are best understood as a third major branch of the sequence-modeling tree, alongside the attention transformers of this Part and the recurrent/SSM alternatives of Beyond Attention: SSMs, Mamba, RWKV & Linear Attention. All three are attempts to escape the \(O(L)\)-serial, \(O(L^2)\)-attention cost of the vanilla transformer; diffusion attacks the serial-depth axis specifically, and pays in approximate factorization and extra compute for the privilege.


Key Takeaways

Key Takeaways

  • Autoregressive generation has a hard serial-depth floor of one forward pass per token; non-autoregressive masked (absorbing-state) diffusion instead denoises all positions in parallel over \(N \ll L\) iterative steps, cutting serial depth and boosting single-stream tokens/sec.
  • The absorbing-diffusion objective reduces to a time-conditioned, mask-rate-weighted cross-entropy on masked positions only — a continuous-time generalization of BERT-style masked LM that is a valid likelihood bound; the weight \(1/(1-\alpha_t)\) is what makes it generative rather than just representation-learning.
  • A single denoising step models masked tokens as conditionally independent, which is wrong for language; iterative remasking (commit high-confidence tokens, re-condition, repeat) recovers inter-token correlations. More steps trade compute for coherence; the remasking schedule is a first-class decoding hyperparameter.
  • Turning your existing GPT into a diffusion LM takes exactly three changes: drop the causal mask, add one [MASK] vocabulary entry, and replace the shifted next-token loss with the unshifted, mask-rate-weighted cross-entropy (the missing shift is the classic first-implementation bug). Everything else — tokenizer, packed shards, optimizer, schedule — is unchanged, which is also why AR-to-diffusion adaptation (Dream, from a Qwen2.5-7B base) is far cheaper than training from scratch.
  • Block diffusion interpolates between AR and diffusion: generate blocks left-to-right (autoregressive, with a block-causal mask) but tokens within a block in parallel (diffusion). This unlocks KV-cache reuse and flexible/arbitrary output length, mapping cleanly onto existing AR serving stacks.
  • The speedup is a serial-depth (latency) win, not a FLOPs win: diffusion often does more total compute per token but in fewer, more parallel steps. The advantage is largest at low-to-moderate batch and long single outputs; at very high concurrency, batched AR throughput catches up.
  • Bidirectionality is a separate, structural benefit: native infilling/editing, global-constraint satisfaction, and robustness to the reversal curse — capabilities AR models must bolt on but diffusion gets for free.
  • Real systems: LLaDA (8B, from scratch) showed diffusion LLMs can rival same-size AR models; Dream (7B) adapts an AR checkpoint to the diffusion objective; Mercury (Inception Labs) is the commercial block-diffusion system pitched on parallel-decode throughput for coding; and Gemini Diffusion (Google DeepMind) and Seed Diffusion (ByteDance) brought the approach to a frontier lab and past ~2,000 tokens/sec.
  • Caveats: likelihoods are ELBO bounds (not directly comparable to AR perplexity), length handling and the post-training/RL stack are less mature, and the kernel/serving ecosystem is still AR-centric. Reach for diffusion when single-stream latency, infilling, or reversal-robustness matter most.

State of the Art & Resources (2026)

Masked-diffusion language models moved from research curiosity to scaled, commercial, and frontier-lab systems across 2024–2026. The frontier is semi-autoregressive block diffusion (for cache reuse and flexible length), AR-to-diffusion adaptation (reusing AR pretraining compute), fast diffusion code models, and the early diffusion-native post-training stack.

Foundational work

  • Austin et al., Structured Denoising Diffusion Models in Discrete State-Spaces (D3PM) (2021) — general discrete-diffusion transition matrices; the absorbing-state special case is the ancestor of masked diffusion.
  • Hoogeboom et al., Argmax Flows and Multinomial Diffusion (2021) — early discrete diffusion for categorical data.
  • Lou, Meng & Ermon, Discrete Diffusion Modeling by Estimating the Ratios of the Data Distribution (SEDD) (2023) — score-entropy objective that made discrete diffusion competitive with AR on likelihood.

The simplified masked-diffusion objective (2024)

  • Sahoo et al., Simple and Effective Masked Diffusion Language Models (MDLM) (2024) — shows the masked-diffusion ELBO reduces to a clean weighted masked cross-entropy.
  • Shi et al., Simplified and Generalized Masked Diffusion for Discrete Data (2024) — parallel derivation of the same simplification.
  • Ou et al., Your Absorbing Discrete Diffusion Secretly Models the Conditional Distributions of Clean Data (RADD) (2024) — explains why explicit time-conditioning is largely redundant.

Scaled systems & semi-AR (2025)

  • Nie et al., Large Language Diffusion Models (LLaDA) (2025) — 8B from-scratch masked-diffusion LLM competitive with same-size AR baselines; demonstrates the reversal-curse advantage.
  • Dream team, Dream 7B (2025) — diffusion LLM adapted from an autoregressive checkpoint with context-adaptive noise and confidence-based decoding.
  • Arriola et al., Block Diffusion: Interpolating Between Autoregressive and Diffusion Language Models (BD3-LM) (2025) — the block-causal hybrid enabling KV-cache reuse and arbitrary-length generation.
  • Inception Labs, Mercury: Ultra-Fast Language Models Based on Diffusion (2025) — commercial diffusion-LLM family (incl. Mercury Coder) built on a block-diffusion architecture; the technical report measures ~1109 tok/s (Mercury Coder Mini) on an NVIDIA H100.
  • Google DeepMind, Gemini Diffusion (2025) — experimental text-diffusion model from a frontier lab, reported at on the order of 1,400 tokens/sec with competitive coding/math benchmarks.
  • Song et al., Seed Diffusion (ByteDance, 2025) — large-scale discrete-diffusion code model reporting >2,000 tokens/sec, among the fastest reported diffusion LLMs.

Post-training & inference acceleration (2025–2026)

  • Zhao et al., d1: Scaling Reasoning in Diffusion Large Language Models via Reinforcement Learning (2025) — introduces diffu-GRPO, substituting a one-forward-pass ELBO surrogate for the policy log-probability so RLVR runs on a masked-diffusion LLM.
  • Zhu et al., LLaDA 1.5: Variance-Reduced Preference Optimization for Large Language Diffusion Models (2025) — DPO-style alignment for diffusion LMs, built entirely around reducing the variance of the ELBO log-prob estimate.
  • Wu et al., Fast-dLLM (NVIDIA, 2025) — training-free acceleration via block-wise approximate KV caching plus confidence-thresholded parallel decoding (adaptive commit counts instead of a fixed schedule).

Code to read

  • kuleshov-group/mdlm and kuleshov-group/bd3lms — reference training implementations of the simplified masked-diffusion ELBO and of block diffusion.
  • ML-GSAI/LLaDA — the 8B reference sampler (low-confidence remasking, semi-autoregressive block decoding) and HuggingFace-loadable weights.

Go deeper

Further Reading

  • Austin, J., et al. (2021). Structured Denoising Diffusion Models in Discrete State-Spaces (D3PM). NeurIPS 2021.
  • Lou, A., Meng, C., & Ermon, S. (2023). Discrete Diffusion Modeling by Estimating the Ratios of the Data Distribution (SEDD). ICML 2024.
  • Sahoo, S., et al. (2024). Simple and Effective Masked Diffusion Language Models (MDLM). NeurIPS 2024.
  • Shi, J., et al. (2024). Simplified and Generalized Masked Diffusion for Discrete Data. NeurIPS 2024.
  • Ou, J., et al. (2024). Your Absorbing Discrete Diffusion Secretly Models the Conditional Distributions of Clean Data (RADD).
  • Nie, S., et al. (2025). Large Language Diffusion Models (LLaDA).
  • Arriola, M., et al. (2025). Block Diffusion: Interpolating Between Autoregressive and Diffusion Language Models (BD3-LM). ICLR 2025.
  • Inception Labs (2025). Mercury: Ultra-Fast Language Models Based on Diffusion. arXiv:2506.17298 — commercial diffusion LLM (incl. Mercury Coder).
  • Google DeepMind (2025). Gemini Diffusion — experimental frontier-lab text-diffusion model.
  • Song, Y., et al. (2025). Seed Diffusion: A Large-Scale Diffusion Language Model with High-Speed Inference. arXiv:2508.02193.
  • Gong, S., et al. (2025). Dream 7B — autoregressive-to-diffusion adaptation from a Qwen2.5-7B base checkpoint.
  • Zhao, S., et al. (2025). d1: Scaling Reasoning in Diffusion Large Language Models via Reinforcement Learning — diffu-GRPO.
  • Zhu, F., et al. (2025). LLaDA 1.5: Variance-Reduced Preference Optimization for Large Language Diffusion Models.
  • Wu, C., et al. (2025). Fast-dLLM: Training-free Acceleration of Diffusion LLM by Enabling KV Cache and Parallel Decoding. NVIDIA.
  • Reference code: kuleshov-group/mdlm, kuleshov-group/bd3lms, ML-GSAI/LLaDA.

Exercises

1. Continuous image-diffusion models almost always inject the timestep \(t\) into the network (via sinusoidal embeddings). Several strong masked-diffusion LLMs, including LLaDA, drop explicit time conditioning entirely and still generate coherent text. What implicit signal lets the network infer “where it is” in the denoising process without being told \(t\), and why does that signal exist specifically in the absorbing-state formulation?

Solution

The implicit clock is the number of [MASK] tokens in the input sequence. In the absorbing forward process each clean token is independently replaced by [MASK] with probability \(1 - \alpha_t\), so the expected fraction of masked positions is \(1 - \alpha_t\), a monotone function of \(t\) (\(\alpha_0 = 1 \Rightarrow\) nothing masked; \(\alpha_1 = 0 \Rightarrow\) everything masked). Because \(\alpha_t\) is monotone, the observed mask count is (in expectation) an invertible readout of \(t\): a sequence that is 90% [MASK] is necessarily near \(t \approx 1\), and one that is 10% [MASK] is near \(t \approx 0\). The model sees this directly on its own input, so feeding \(t\) separately is largely redundant.

This works because the corruption is absorbing. The only transition is data \(\to\) [MASK]; there is never mask \(\to\) data or data \(\to\) different-data. So the mask count only ever grows with \(t\) and cleanly encodes the noise level. In a general D3PM process where any token can flip to any other token, the “amount of corruption” is not visible from a simple count — a corrupted token looks like an ordinary token — so the mask-count clock does not exist and explicit time conditioning is far more necessary. (See the “Time conditioning, or the lack of it” section and the RADD result of Ou et al., 2024.)

2. A dense \(3\text{B}\)-parameter model runs in bf16 (\(b = 2\) bytes/param) on a GPU with HBM bandwidth \(\beta = 2.0\) TB/s. Using the decode-step memory floor \(t_{\text{step}} \gtrsim P b / \beta\), compute, for a single sequence of \(L = 256\) output tokens:

(a) the autoregressive wall-clock time and tokens/sec;

(b) the diffusion wall-clock time and tokens/sec for \(N = 32\) denoising steps, taking \(t_{\text{step}}' = 9\) ms per (compute-heavier) parallel step;

© the single-stream latency speedup of diffusion over AR.

Solution

The per-step memory floor is $$ t_{\text{step}} \gtrsim \frac{P \cdot b}{\beta} = \frac{3 \times 10^9 \times 2}{2.0 \times 10^{12}} = \frac{6 \times 10^9}{2.0 \times 10^{12}} = 3.0 \times 10^{-3} \text{s} = 3 \text{ms}. $$

(a) Autoregressive: one forward pass per token, so \(T_{\text{AR}} = L \cdot t_{\text{step}} = 256 \times 3\ \text{ms} = 768\ \text{ms} = 0.768\ \text{s}\). Throughput \(= 256 / 0.768 \approx \mathbf{333}\) tokens/s.

(b) Diffusion: serial depth is \(N\), not \(L\), so \(T_{\text{NAR}} = N \cdot t_{\text{step}}' = 32 \times 9\ \text{ms} = 288\ \text{ms} = 0.288\ \text{s}\). Throughput \(= 256 / 0.288 \approx \mathbf{889}\) tokens/s.

© Speedup \(= T_{\text{AR}} / T_{\text{NAR}} = 768 / 288 \approx \mathbf{2.67\times}\).

The win comes entirely from cutting serial depth from \(256\) to \(32\); each diffusion step is 3x more expensive (\(9\) ms vs \(3\) ms) because it processes all \(256\) positions in parallel, which is why the speedup (\(2.67\times\)) is well below the \(8\times\) reduction in step count.

3. Consider a block-diffusion model generating \(L = 256\) tokens with block size \(B = 16\) and \(N_B = 4\) denoising steps per block. Compute (a) the number of blocks, (b) the total number of block-denoising forward passes and the resulting serial depth, © how the serial depth compares to the equivalent AR model, and (d) the total position-evaluations by the denoiser versus AR. What does the comparison of © and (d) tell you about the nature of the diffusion speedup?

Solution

(a) Number of blocks \(= L / B = 256 / 16 = \mathbf{16}\) blocks.

(b) Blocks are generated left-to-right (autoregressively), and each block runs \(N_B\) denoising steps, so total block-denoising passes \(= 16 \times 4 = \mathbf{64}\). Because the blocks are sequential and the steps within a block are sequential, the serial depth is exactly \(\mathbf{64}\) dependent steps.

© The equivalent AR model needs \(L = 256\) sequential single-token passes. Serial depth drops \(256 \to 64\), a \(\mathbf{4\times}\) reduction — this is the single-stream latency win.

(d) Each of the \(64\) passes evaluates a block’s worth of \(B = 16\) positions, so total position-evaluations \(\approx 64 \times 16 = \mathbf{1024}\), versus AR’s \(256\) — about \(\mathbf{4\times}\) more position-evaluations. (With KV-cache reuse the finalized earlier blocks are not recomputed, so the extra cost is the within-block MLP work on the \(16\) live positions across \(4\) steps.)

The lesson: the diffusion speedup is a serial-depth (latency) win, not a FLOPs win. Here it reduces dependent steps by \(4\times\) while doing \(4\times\) more total position-evaluation work. That extra compute is nearly free when the GPU is otherwise idle (low batch) and expensive when it is already saturated (high batch) — the whole economic trade-off in one calculation.

4. Within a single denoising step the model predicts every masked position independently: \(p_\theta(x_0 \mid x_t) = \prod_i p_\theta(x_0^i \mid x_t)\). Explain why this factorization is wrong for natural language, what failure you would observe if you filled all masked positions in a single step, and how the iterative remasking loop recovers the missing structure. What choice of \(N\) (number of steps) makes the error vanish, and what is the cost?

Solution

Why it is wrong. The true joint distribution over the masked tokens is highly correlated — the identity of one masked token strongly constrains the others (subject/verb agreement, matching brackets, a noun that must agree with an earlier article). A product of per-position marginals \(\prod_i p_\theta(x_0^i \mid x_t)\) throws away all of that cross-position dependence and treats the masked tokens as if they were independent.

Failure mode. If you sample every masked position at once from this factorized distribution, each token is individually plausible given the context, but the tokens are jointly incoherent — a subject and verb that disagree, a pronoun with the wrong antecedent, code that is locally valid but globally inconsistent. This is the direct analog of why you cannot sample all pixels of an image independently in one shot.

How remasking fixes it. The sampler never trusts a single step to fill everything. Each step commits only a subset of predictions (e.g. the highest-confidence ones under confidence-based remasking) and returns the rest to [MASK]. Crucially, each committed token becomes bidirectional context for the next step, so the next step’s per-position marginals are conditioned on the already-placed tokens. Over \(N\) steps this re-conditioning reintroduces the inter-token correlations that any one step’s factorization discards.

When the error vanishes, and the cost. With \(N = L\) and a one-token-per-step schedule, exactly one position is committed per step conditioned on all previously committed tokens — this is an (order-flexible) autoregressive factorization, which is exact, so the conditional-independence error disappears and coherence matches AR. The cost is that serial depth returns to \(L\): you have thrown away the entire speed advantage. Production systems pick the smallest \(N\) (empirically \(\sim L/4\) to \(L/2\), or a few steps per block) that still holds quality. More steps trade compute for coherence.

5. The sampler in the chapter uses confidence-based remasking: each step it keeps the n_keep highest-confidence predictions. Modify the sampler to implement random remasking instead — keep a uniformly random subset of the currently-masked positions of size n_keep. Give the replacement code block and explain, in terms of the chapter’s argument, why confidence-based remasking generally produces better text at the same step budget.

Solution

Only the selection of which masked positions to commit changes; the schedule (masked_target, n_keep) and the prediction of pred/x_candidate stay identical. Replace the confidence-ranking block (the torch.topk(conf_masked, n_keep) branch) with a random draw among the currently-masked positions:

    # Random remasking: keep a uniformly random subset of the masked
    # positions of size n_keep, remask the rest.
    masked_idx = masked.nonzero(as_tuple=True)[0]   # indices still masked
    if n_keep > 0:
        perm = masked_idx[torch.randperm(masked_idx.numel(), device=device)]
        keep_idx = perm[:n_keep]                     # random subset to commit
        new_x = x.clone()
        new_x[masked] = MASK_ID                      # provisionally remask all
        new_x[keep_idx] = x_candidate[keep_idx]      # commit the random ones
        x = new_x

Note this no longer needs conf at all for the selection (it is still computed to build x_candidate, and you could skip it under greedy decoding). The conf_masked/topk lines are dropped.

Why confidence-based is better at the same \(N\). The whole engine of iterative denoising is that committed tokens become context that sharpens the conditionals for everything still masked. Confidence-based remasking commits exactly the positions the model is most sure about — the ones least likely to be wrong — so each round adds reliable context and defers the genuinely ambiguous positions until they have accumulated more surrounding evidence. Random remasking, by contrast, will often lock in a low-confidence (likely-wrong) guess early; because absorbing diffusion never un-commits a token, that mistake becomes permanent, misleading context for the remaining predictions. So at a fixed step budget confidence-based remasking wastes fewer commitments on bad guesses and produces more coherent output; random remasking needs more steps to reach comparable quality (the chapter notes it “wastes steps committing to low-confidence guesses early”).

6. Implement the training-side counterpart of the sampler: a function masked_diffusion_loss(denoiser, x0, alpha_fn) that returns one Monte-Carlo estimate of the absorbing-diffusion loss $$ \mathcal{L}(\theta) = \mathbb{E}{t \sim \mathcal{U}(0,1)}\, \mathbb{E} -\log p_\theta(x_0^i \mid x_t) \right] $$ for a single clean sequence }\left[ \frac{1}{1-\alpha_t} \sum_{i:\, x_t^i = \texttt{[MASK]}x0. Follow the chapter’s conventions (MASK_ID, a denoiser returning (L, V) logits). Then answer: which positions contribute to the loss, and what would go wrong if you dropped the \(1/(1-\alpha_t)\) weight?

Solution

A single Monte-Carlo sample: draw one time \(t\), mask each token independently with probability \(1 - \alpha_t\), run one bidirectional forward pass, and take a weighted cross-entropy over the masked positions only.

import torch
import torch.nn.functional as F

MASK_ID = 0  # same absorbing-token convention as the sampler

def masked_diffusion_loss(denoiser, x0, alpha_fn, device="cpu"):
    """
    One Monte-Carlo estimate of the absorbing-diffusion loss for a clean
    sequence x0 (LongTensor[L]). `alpha_fn(t)` maps t in [0,1] to the
    survival probability alpha_t (alpha_0 = 1, alpha_1 = 0).
    `denoiser(xt)` returns (L, V) logits over clean tokens.
    """
    x0 = x0.to(device)
    L = x0.numel()

    # 1. Sample a time t ~ U(0,1) and its survival probability.
    t = torch.rand(1, device=device).item()
    alpha_t = alpha_fn(t)                          # scalar in [0, 1]

    # 2. Mask each token independently with prob (1 - alpha_t): data -> [MASK].
    keep = torch.rand(L, device=device) < alpha_t  # True = survives unmasked
    xt = x0.clone()
    xt[~keep] = MASK_ID
    masked = ~keep                                 # positions that got masked

    # Degenerate draw: nothing masked -> no loss signal this sample.
    if masked.sum() == 0:
        return torch.zeros((), device=device)

    # 3. One bidirectional pass predicting the clean token at every position.
    logits = denoiser(xt)                          # (L, V), no causal mask

    # 4. Cross-entropy on MASKED positions only (unmasked = context, no loss).
    ce = F.cross_entropy(logits[masked], x0[masked], reduction="sum")

    # 5. Weight by 1 / (1 - alpha_t) to make the masked-LM loss a valid
    #    negative-log-likelihood bound across all masking rates.
    return ce / (1.0 - alpha_t)

A linear schedule alpha_fn = lambda t: 1.0 - t is the common default (then \(1 - \alpha_t = t\), so the weight is \(1/t\)).

Which positions contribute. Only the masked positions (logits[masked], x0[masked]) enter the cross-entropy. Unmasked positions are fed to the network as context but carry no loss term — the model is rewarded solely for reconstructing what was hidden. This mirrors point 1 of the objective discussion and matches the sampler, which never overwrites clamped/committed context.

Why the \(1/(1-\alpha_t)\) weight matters. It corrects for the masking rate so that the expectation over \(t\) forms a tight Evidence Lower Bound on the true negative log-likelihood (MDLM/RADD). Without it you would just be averaging BERT-style masked cross-entropy uniformly over masking rates, which is a representation-learning objective, not a proper generative one. Concretely, low-\(t\) samples mask only a few tokens; the large weight \(1/(1-\alpha_t)\) makes each of those rare masked positions count heavily, while high-\(t\) samples (almost everything masked, weight \(\approx 1\)) are the hard denoising regime. Dropping the weight mis-balances these regimes, so the loss no longer bounds the likelihood and the model is not trained to be a valid generator across the full \(0\% \to 100\%\) masking range that iterative sampling actually traverses.