~/satyajit

Flash-dLLM: a KV cache for keys that never sit still, and a model that checks its own drafts

mdjsonmcp

2026-09-26 · 22 min · diffusion · llm · language-models · kv-cache · inference-optimization · speculative-decoding · explainer

An autoregressive model can cache keys and values because the past never changes. Once token 41 is written, its key in layer 12 is the same at step 500 as it was at step 42. A masked diffusion LLM breaks that. LLaDA and Dream attend in both directions, so when a denoising step commits one token, the hidden state of every other position can move, and with it every key and value in every layer above the first. There is no exact cache for that model. There are approximations, and a bill for keeping them fresh.

Flash-dLLM comes from Quan Nguyen-Tri, Mukul Ranjan and Zhiqiang Shen at MBZUAI's VILA Lab, the group behind Elastic-Cache, one of the baselines it beats. It changes two things, and both are about moving fewer bytes. Flash-Cache is a Triton kernel that projects, rotates and writes keys and values straight into a flat, preallocated cache, and recomputes only a masked window plus a fixed budget of the most-attended decoded tokens each step. Flash-Verify lets the model check its own low-confidence guesses in one small extra forward pass, with no draft model. On LLaDA-1.5 on one A100 it reports 148.0 to 210.6 tokens/s across four benchmarks, and 5.1x and 11.0x the throughput of Elastic-Cache on GSM8K and HumanEval at 512 generated tokens.

I read the paper and the released code. The code implements the paper's mechanism, with two differences that matter the moment you touch a threshold. The headline numbers are what the tables say; what they are measured against is the part to read slowly.

What it istraining-free inference for masked diffusion LLMs: an IO-aware KV cache plus self-verified parallel decoding
Model testedLLaDA-1.5 (8B), and only LLaDA-1.5
Hardwareone A100 80GB for every benchmark; the kernel microbenchmark is on an RTX 3090
BenchmarksGSM8K (5-shot), MATH (4-shot), HumanEval (0-shot), MBPP (3-shot), at 256 and 512 generated tokens, batch 32
Headline210.6 tokens/s at 83.02% on GSM8K-512, against 41.7 tokens/s at 82.79% for Elastic-Cache
CodeVILA-Lab/Flash-dLLM, Apache-2.0; kernels in llada/flash_cache_triton.py (644 lines), the decode loop in llada/generate.py (474 lines)
VILA-Lab/Flash-dLLM@7437a55 · snapshot 2026-09-26
tracked files
24
license
Apache-2.0
branch
main
tests
none found
source
151.3 kB
commit date
2026-09-22
source by language
Python143.7 kB(8)Shell7.6 kB(4)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-09-26 at 7437a55 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile

Why a diffusion LLM will not hold still

A masked diffusion LM generates by filling in blanks. It appends a run of [MASK] tokens to the prompt, predicts every masked position at once, commits some of those predictions, and repeats until nothing is masked. The iLLaDA write-up covers the objective, and Dream-Cubed runs the same absorbing-mask process on Minecraft blocks. For caching, one property matters: attention is bidirectional. Position ii at layer ll reads every other position, including masks that become real tokens two steps later.

So when step tt commits position jj, the input at jj changes, and every hidden state that attended to jj changes with it. In an autoregressive model the KV cache is exact by construction. Here it is stale by construction. Every dLLM cache makes the same move, which the paper writes as its Eq. (1):

A[Qt]t,l=softmax ⁣(Q[Qt]t,l (K~t,l)⊤dk)V~t,l,K~[Qt]t,l←K[Qt]t,l,V~[Qt]t,l←V[Qt]t,l\mathbf{A}^{t,l}_{[\mathcal{Q}^t]} = \mathrm{softmax}\!\left(\frac{\mathbf{Q}^{t,l}_{[\mathcal{Q}^t]}\,(\tilde{\mathbf{K}}^{t,l})^{\top}}{\sqrt{d_k}}\right)\tilde{\mathbf{V}}^{t,l}, \qquad \tilde{\mathbf{K}}^{t,l}_{[\mathcal{Q}^t]} \leftarrow \mathbf{K}^{t,l}_{[\mathcal{Q}^t]},\quad \tilde{\mathbf{V}}^{t,l}_{[\mathcal{Q}^t]} \leftarrow \mathbf{V}^{t,l}_{[\mathcal{Q}^t]}

K~\tilde{\mathbf K} and V~\tilde{\mathbf V} are the cached keys and values for every position in layer ll. Qt\mathcal Q^t is the set of positions recomputed at step tt: their queries attend to the whole cache, and their fresh keys and values overwrite their own rows. Rows in Qt\mathcal Q^t are exact. Every other row is whatever it was the last time it was in Q\mathcal Q. The whole design space is two choices: which positions go into Qt\mathcal Q^t, and how often everything gets recomputed.

What came before

The prior methods differ in those two choices.

The Flash-dLLM paper notes two things they share: the cache update is PyTorch, one kernel launch per operation, and caching and parallel decoding are studied separately.

Parallel decoding has its own approximation. At one step, the model predicts every masked position from the same partially masked input xt\mathbf x_t, so committing several at once samples from a product of marginals, ∏ipθ(xi∣xt)\prod_i p_\theta(x^i \mid \mathbf x_t), while the true joint has dependencies between them. Fast-dLLM's confidence-aware decoding commits position ii only when its confidence ci=max⁡xpθ(xi∣xt)c^i = \max_x p_\theta(x^i \mid \mathbf x_t) clears a threshold ϵ\epsilon, usually 0.9. Fast-dLLM's theorem, restated in Flash-dLLM's appendix, is what makes that safe: if nn positions each have confidence above 1−δ1-\delta with (n+1)δ≤1(n+1)\delta \le 1, the product's mode is the joint's mode and the two are within 3n−12δ\tfrac{3n-1}{2}\delta in total variation.

The cost is a throughput ceiling. On an uncertain stretch few tokens clear 0.9, the step count stays high, and many tokens thrown back were already right. The paper's Figure 1(c) reports a correlation of r = 0.48 between average confidence and the number of early-decodable tokens across denoising steps: moderate, and the motivation for Flash-Verify.

Where the time goes: four kernels and a round trip

Take one layer of a cached step. The conventional path launches four kernels for the query set: QKV projection, rotary position embedding, the cache write, and attention. Each writes its output to HBM and the next reads it back. With QQ query tokens, a sequence of NN and hidden size dmodeld_\text{model}, the paper puts per-layer traffic at roughly 4×O(Q dmodel)+O(Nd)4 \times \mathcal O(Q\,d_\text{model}) + \mathcal O(N d). The three O(Q dmodel)\mathcal O(Q\,d_\text{model}) steps do almost no arithmetic per byte, so they run at memory speed.

A stacked bar chart of per-layer time in milliseconds. The conventional cache path stacks linear projection, positional embedding, gather positions and attention to about 0.57 ms, with positional embedding the largest segment. The Flash-Cache bar stacks the fused kernel and flash attention to about 0.42 ms, annotated 1.37x.
Per-layer latency on LLaDA-1.5: in the conventional cache path, positional embedding and the cache gather take more time than attention does. Fusing them gives 1.37x per layer. This is a single-layer microbenchmark on an RTX 3090, not the A100 the benchmarks ran on (Flash-dLLM paper, Figure 1a).

In that chart, attention is only the top slice of the conventional bar; rotary embedding is the largest. Part of that is bytes. Part is PyTorch: several small launches per operation, and LLaDA's reference RoPE runs in fp32 (rope_full_precision is true in its config.json), so qq and kk are cast up and back down.

The fix borrows FlashAttention's move: keep the intermediate in SRAM. The fused kernel loads a block of hidden states, computes qq, kk and vv for one head, applies RoPE to the accumulators in registers, and writes kk and vv directly into the rows of the cache they belong to. No intermediate key or value tensor ever reaches HBM.

A data-flow diagram across GPU SRAM and GPU HBM. The conventional flow reads the input from HBM into a linear projection, writes current KV back to HBM, round-trips it through positional embedding and a gather step, then writes the KV cache. The Flash-Cache flow reads the input once into a fused kernel in SRAM and writes directly to the KV cache.
The conventional flow round-trips an intermediate current-KV tensor through HBM between its steps; the fused kernel reads the input once and writes the cache once (Flash-dLLM paper, Figure 3a).

The code does what the diagram says. _flash_qkv_proj_cache_fwd runs one Triton program per 16-row query block and head, accumulates the projection in fp32 over 64-wide slices of dmodeld_\text{model}, and scatters the result by absolute position:

# llada/flash_cache_triton.py, _flash_qkv_proj_cache_fwd (abridged)
pos = tl.load(Pos + offs_m, mask=q_mask, other=0.0)   # each query token's row in the flat cache
offs_k = pos[:, None] * D_MODEL + offs_h[None, :]     # where its key and value belong
# ... projection accumulated in fp32 over D_MODEL ...
rk1 = acc_k1 * cos1 - acc_k2 * sin1                    # RoPE on the accumulators, in registers
rk2 = acc_k2 * cos2 + acc_k1 * sin2
tl.store(K + offs_k, rk1, mask=q_mask[:, None])       # straight into the cache
tl.store(K + offs_k + HALF, rk2, mask=q_mask[:, None])
tl.store(V + offs_k, acc_v1, mask=q_mask[:, None])
tl.store(V + offs_k + HALF, acc_v2, mask=q_mask[:, None])

Two honest limits. Attention is still a separate kernel: the fusion covers projection, RoPE and the write, and the O(Nd)\mathcal O(N d) stream over the cache is untouched. And attention tracking is not free. _flash_masked_attention_fwd writes every raw q⋅kq \cdot k logit for the 16 decoding-candidate rows into an fp32 buffer S shaped (block_m, n_heads, batch, max_length), which PyTorch then averages. That is HBM traffic the paper's accounting does not mention.

A flat cache and a block table

The cache is one tensor per layer for keys and one for values, each (batch × max_length, d_model), allocated once before decoding (generate.py, lines 179 and 180). LLaDA-8B uses plain multi-head attention with 32 layers and dmodel=4096d_\text{model} = 4096, so each cached position costs 2×4096×22 \times 4096 \times 2 bytes =16= 16 KiB per layer and 512 KiB across the model in bf16 (my arithmetic from the config). The flat layout does not shrink that. It removes allocator churn and the padding of the usual four-dimensional cache, which the paper credits for the memory gap below.

Every query is cut into 16-row blocks, and a block table records each block's sample, key length and query range (make_blocks, read by the kernels as bt_row = block_table + pid_m * 4). One sample can be in its full first pass while another runs a cached step, in the same launch, with no padding; the paper calls this scheduled flash attention. The code also uses it for something the paper only calls "adding or removing blocks": when a sample finishes, generate.py puts the next prompt into its slot mid-run (lines 405 to 427), and each sample stops once every position before its end-of-sequence token is decoded. That is continuous batching, and part of why throughput scales with batch size. The paper says the baselines were re-run on identical hardware and software; it does not say whether their loops refill slots.

Which rows get recomputed

Figure 1(b) of the paper is the observation behind the refresh policy: the 32 most-attended decoded tokens capture up to about half of all attention weight in the middle layers, 5 to 20. If a few rows carry most of the attention, recomputing those rows buys most of the accuracy.

So the query at each step after the first is a masked window plus a tracking set, Qt+1=Mβmt+1∪Tt+1\mathcal Q^{t+1} = \mathcal M^{t+1}_{\beta_m} \cup \mathcal T^{t+1}. The tracking set holds the tokens decoded at the last step plus the highest-scoring previously decoded tokens, up to a budget βt\beta_t, scored by the paper's Eq. (2):

ait=∑l=1L1H∑h=1H1∣Mβmt∣∑j∈MβmtSj,it,l,ha_i^t = \sum_{l=1}^{L}\frac{1}{H}\sum_{h=1}^{H}\frac{1}{|\mathcal M^t_{\beta_m}|}\sum_{j\in\mathcal M^t_{\beta_m}} \mathbf S^{t,l,h}_{j,i}

Sj,it,l,h\mathbf S^{t,l,h}_{j,i} is the unnormalized attention logit from masked query jj to decoded key ii in head hh of layer ll, with HH heads and LL layers: how much the positions still being decided look at token ii.

Here is what generate.py does with the settings its scripts use for Table 3 (blocks of 16, mask_num=4, track_num=4):

That is 128 recomputed rows per sample per step, against a full sequence of the prompt plus 512 masks. The toy below runs the three policies on the same decode schedule so you can see what each one leaves stale.

which K/V rows are recomputed, step by steptoy: 8 prompt + 24 generated positions · not measured
A grid of 14 denoising steps by 32 positions under the window + tracked (Flash-Cache) policy. On average 10.4 of 32 key/value rows are recomputed per step after the first, 37 percent of the no-cache work.promptgenerated (starts as [MASK])t=0t=2t=4t=6t=8t=10t=12
recomputed this step reused, 1–2 steps old reused, older committed, but its row was cached as [MASK]· = still masked · ✓ = committed this step
rows recomputed / step
10.4 of 32, after t=0
work vs no cache
37%
at t = 6
oldest row 6 steps · 0 [MASK]-stale

t = 6: 12 rows recomputed: window 4, look-ahead 4, just committed 2, tracked by score 2.

With the Flash-Cache policy and a budget of 4, the toy recomputes about a third of what no cache would, at a steady rate per step. The price shows in the prompt columns: rows that never rank are never refreshed. Block refresh does similar total work in bursts and bounds staleness at one block. Drag the budget to 0 and the committed tokens turn red: their rows were cached while they were still [MASK] and never updated. That is why fresh commits get the first tracking slots, and why the keep_num detail matters.

One question the paper leaves open. Elastic-Cache, from the same authors, observed that the most-attended token is the one whose keys and values drift least. Flash-Cache spends its budget recomputing exactly those tokens. Both can be right, because the error a stale row injects scales roughly with its attention weight times its drift. But no ablation compares attention-ranked tracking against a random or most-recent set of the same size. Table 4 only varies the budget: going from 48 to 96 tracked tokens raises GSM8K accuracy by 0.83 to 1.69 points and costs 10.9% to 20.3% of throughput.

Flash-Verify: the model checks its own drafts

Two-panel overview. Left, Flash-Cache: a query of tracked tokens and masked tokens attends bidirectionally to the full KV cache; attention scores to decoded tokens are accumulated across layers and the top-k become the next tracked tokens. Right, Flash-Verify: drafts are sorted by confidence, those above epsilon are decoded directly, the remaining drafts are duplicated as a draft view and a masked view under a causal bidirectional mask, and tokens are accepted in causality order while both views agree and the mask-view confidence clears gamma.
Left, Flash-Cache: tracked plus masked tokens attend to the full cache, and the most-attended decoded tokens are re-selected every step. Right, Flash-Verify: low-confidence drafts are fed back twice, as drafts and as masks, and accepted left to right while the two views agree (Flash-dLLM paper, Figure 2).

Flash-Verify adds a second, small forward pass to each step. The first is the normal cached step, whose window predictions are sorted by confidence: positions with ci≥ϵc^i \ge \epsilon form the confident set Dt\mathcal D^t and are committed directly, and the rest form the search set St\mathcal S^t.

The verify pass builds a new query from three groups: a tracked context (previously decoded tokens plus Dt\mathcal D^t), the search positions filled with their drafts (the draft view), and the same positions filled with [MASK] (the mask view). Both views share rotary positions; a custom mask keeps them apart, and the tracked context cannot see the draft view. Sort the search positions by draft confidence as i1,…,iKi_1, \dots, i_K; the mask view at iki_k sees the drafts at i1i_1 to ik−1i_{k-1} and the masks from iki_k onward, never its own draft. So its output is

pik(x)=pθ ⁣(xik=x | x+t, x^i1,…,x^ik−1),p_{i_k}(x) = p_\theta\!\left(x^{i_k} = x \,\middle|\, \mathbf x^t_{+},\ \hat x^{i_1}, \dots, \hat x^{i_{k-1}}\right),

where x+t\mathbf x^t_+ is the input with Dt\mathcal D^t committed and x^\hat x are the drafts. That is the conditional that one-token-at-a-time decoding in this order would see at its kk-th step, which is the same object speculative decoding verifies against (EAGLE-3 and Uno cover that lineage). Here the drafter and the verifier are one model, and the verify query costs 32 rows, not a sequence.

The paper's acceptance rule (Eq. 5) walks the search set in that order and stops at the first failure:

accept(i)=I ⁣[x^i=x~i]⋅I ⁣[c~i≥γ]\mathrm{accept}(i) = \mathbb I\!\left[\hat x^i = \tilde x^i\right]\cdot \mathbb I\!\left[\tilde c^i \ge \gamma\right]

x~i\tilde x^i and c~i\tilde c^i are the mask view's top token and its probability, and γ\gamma is the verify threshold. Appendix C proves the guarantee this buys, measured against the model's own sequential distribution rather than the data: an accepted token is within 1−c~i≤1−γ1 - \tilde c^i \le 1 - \gamma of it in total variation, and a block of mm accepted tokens within 1−γm≤m(1−γ)1 - \gamma^m \le m(1-\gamma). Tokens committed by confidence alone are outside this bound; they fall under Fast-dLLM's theorem.

The code's rule is stricter

This is the first difference between paper and code, in generate.py:

# llada/generate.py, lines 320-328 (abridged)
p_verify = F.softmax(logits_verify_j.to(torch.float64), dim=-1)
x0_p_verify = p_verify.gather(1, x_verify_j.unsqueeze(1)).view(-1)  # mask view's prob. of each draft
x0_p_verify = x0_p_verify.cumprod(dim=0)                             # running product, causality order
keep_idx = (x0_p_verify >= gamma)
keep_num = keep_idx.sum().item()

The code thresholds the running product of the mask view's probabilities of the drafts. Each factor is at most 1, so a product at or above γ\gamma means every factor is too, and for γ>0.5\gamma > 0.5 a token with probability at least γ\gamma must be the top token, so agreement is implied. The code therefore accepts a prefix of what the paper's rule accepts, and that prefix meets the block bound 1−γ1 - \gamma directly instead of 1−γm1 - \gamma^m. The README warns that the two definitions are not interchangeable. The released scripts carry Table 3's thresholds, so if the tables came from this code, their γ\gamma values are code-rule values; port the paper's rule and re-tune.

The other limits in the code: the search set is capped at 8 (min(num_verify, block_m // 2)), the verify query is always 32 rows, so the tracked context in the verify pass shrinks to 32−2∣St∣32 - 2|\mathcal S^t|, the verify pass reads the cache but never writes it, and at least one token is committed every step. Try both rules on a toy window:

one Flash-Verify step, one 16-token windowtoy probabilities · not measured
Rong0.97saves0.93200.88coins0.95per0.99month0.98,0.91[M]0.62in0.74[M]0.70year0.84,0.79[M]0.55[M]0.48[M]0.33[M]0.25
committed: draft confidence ≥ ε committed by verification searched, stays maskeddashed = outside the search set · small number = draft confidence
orderdraftdraft cmask view prunning ∏papercode
1200.880.970.970acceptaccept
2year0.840.950.921acceptaccept
3,0.790.930.857acceptaccept
4in0.740.960.823acceptaccept
5one0.700.900.740acceptstop
6so0.620.720.533stopstop
7he0.550.880.469stopstop
8saves (mask view disagrees)0.480.410.192stopstop
Verify-pass attention mask for 8 search tokens. Rows are queries, columns are keys. The tracked context T sees itself and the mask view but no draft. Draft j sees earlier-or-equal drafts and later masks. Mask j sees earlier drafts and itself and later masks, never its own draft.TTd1d1d2d2d3d3d4d4d5d5d6d6d7d7d8d8m1m1m2m2m3m3m4m4m5m5m6m6m7m7m8m8verify mask: T = tracked context, d = draft view, m = mask view; filled = may attend
confidence only
6 tokens this step
+ Flash-Verify
10 (code rule; paper rule gives 11)
verify query, 32 rows
16 tracked + 8 drafts + 8 masks

At the defaults (ϵ=0.9\epsilon = 0.9, γ=0.8\gamma = 0.8), six positions clear the confidence threshold. The paper's rule adds five from the search set before " so" fails at 0.72. The code's rule adds four, because the running product drops to 0.740 at the fifth. Lower ϵ\epsilon and more tokens are committed without verification; raise it and more of the step's work moves to the verify pass.

The reported effect is in Table 6 of the paper: on GSM8K-512 with Flash-Cache, greedy decoding commits 1.0 token per step, confidence-aware decoding 2.8, and Flash-Verify 5.7. Twice the tokens per step does not mean twice the speed, since each step now runs two forward passes: at batch 32, Flash-Verify reaches 199.8 tokens/s against 139.5 for confidence-aware decoding in that table.

A combined chart against the threshold value, 0.60 to 0.90. Bars give tokens decoded per iteration: Flash-Verify 7.2, 6.4, 6.1, 5.7, 5.2, 4.8; confidence-aware 5.6, 4.6, 3.6, 3.2, 2.8 at the thresholds it was run at. Lines give accuracy and throughput for both, with dashed greedy reference lines.
Tokens per iteration, accuracy and throughput as the thresholds move: Flash-Verify decodes more tokens per step at every setting, and its throughput is at or above confidence-aware decoding's (Flash-dLLM paper, Figure 6).

The paper reads this figure as a matched comparison: Flash-Verify at 7.2 tokens per iteration runs at about the throughput of confidence-aware decoding at 5.6, with 3.5% higher accuracy, and its throughput advantage grows to 1.33x as more tokens are decoded per iteration.

The numbers

All from Table 1 of the paper: LLaDA-1.5, one A100 80GB, batch 32, throughput in tokens/s. The last column is my division of two of the paper's numbers.

BenchmarkLenFast-dLLMElastic-CacheFlash-Cache+ Flash-VerifyAccuracy, + Flash-VerifyBest accuracy in rowvs Elastic-Cache
GSM8K25651.245.9144.9194.981.8883.624.2x
GSM8K51236.841.7149.4210.683.0283.025.1x
MATH25644.440.6144.3189.736.5637.224.7x
MATH51244.441.4149.9210.135.9837.765.1x
HumanEval25618.720.9169.4209.239.6343.2910.0x
HumanEval51215.416.8145.7185.640.2442.0711.0x
MBPP25628.032.7115.9148.038.2041.804.5x
MBPP51217.832.8102.2148.239.0040.204.5x

"Flash-Cache" here is Flash-Cache with confidence-aware decoding, the same decoding family as Fast-dLLM and Elastic-Cache. Four readings.

Most of the gap is the cache, not the verifier. On GSM8K-512, Flash-Cache alone is 149.4 tokens/s against Elastic-Cache's 41.7 and Fast-dLLM's 36.8. Flash-Verify then adds a further 1.41x by my division, and the paper puts its gain across the eight settings at 23.5% to 45.0%. The paper has no ablation that splits the cache's share into kernel fusion, selective refresh and scheduling. The only isolated kernel number is the 1.37x single-layer microbenchmark, on a different GPU.

The 81.0x and 148.2x are against the slowest thing in the table. They are relative to greedy decoding with no cache: one token per step, the full sequence recomputed every step, 2.6 tokens/s on GSM8K-512 and 1.0 on MBPP-512. They say how slow naive diffusion decoding is.

Accuracy is a trade, by task. Flash-Cache plus Flash-Verify is the best configuration on GSM8K-512 and within 1.78 points of the row's best on the math tasks. It gives up 3.66 points on HumanEval-256 and 3.60 on MBPP-256. Table 1 reports no variance, and the paper's five-seed sweep in Table 4 shows standard deviations up to 1.03 points on GSM8K, so gaps under a point are inside the noise. One curiosity: greedy decoding with Flash-Cache is more accurate than greedy decoding without a cache on seven of the eight settings (37.22 against 33.52 on MATH-256). The approximate cache is not costing accuracy here, and the paper does not explain why it sometimes helps.

The extra baselines are slower. Table 2 adds dKV-Cache, FlashDLM, dLLM-Cache, Dyna-dLLM and FreeDave on GSM8K-512: 14.9 to 42.8 tokens/s, at 79.32% to 81.50%.

Two line charts against batch size 1 to 32 on GSM8K-512 with 1-shot prompts. Left, throughput: Flash-dLLM rises to roughly 475 tokens/s at batch 24 and dips at 32; Fast-dLLM stays near 100 and stops at 16; dLLM-Cache and LLaDA-1.5 stay flat and low; Llama3 rises past Flash-dLLM at the largest batches with 53.98% accuracy. Right, peak memory: Fast-dLLM reaches the 80 GB line at batch 24, Flash-dLLM grows to about 36 GB at batch 32.
Throughput (left) and peak memory (right) against batch size on GSM8K-512 with 1-shot prompts. Fast-dLLM runs out of memory at batch 24; Llama3-8B is an autoregressive reference, not an accuracy-matched baseline (Flash-dLLM paper, Figure 4).

Memory is the cleanest win. At batch 16 the paper reports about 26 GB for Flash-dLLM against 50 GB for Fast-dLLM, about 48% less, and Fast-dLLM runs out of the A100's 80 GB at batch 24.

Throughput scaling needs a closer look. The text says Flash-dLLM "scales nearly linearly" to batch 32, but in the left panel its curve peaks at batch 24 and dips at 32. Table 6, with 5-shot prompts, flattens: 186.2 tokens/s at batch 16 is 93.2% of the 199.8 at batch 32. And the figure's configuration is not stated. Read off the chart, Flash-dLLM runs at roughly 430 tokens/s at batch 16 with 78.62% accuracy, while Table 5(b), the same 1-shot GSM8K-512 setting at batch 16, reports 234.4 tokens/s at 80.12% for Flash-Cache plus Flash-Verify. The figure and the table are not the same configuration, and the paper does not say what differs.

What to keep in mind

None of this undercuts the idea. A diffusion LLM's cache is always wrong somewhere; the engineering question is how cheaply you can keep the rows that matter most close to right. Flash-dLLM's answer is to make "recompute these 128 rows" one fused pass over a flat cache, and to spend part of what that saves on a second, 32-row pass that turns the model's near-misses into committed tokens. Most of the reported speed comes from the first half. The second half is the more original idea.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Flash-dLLM: a KV cache for keys that never sit still, and a model that checks its own drafts", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026flashdllm,
  author = {Satyajit Ghana},
  title  = {Flash-dLLM: a KV cache for keys that never sit still, and a model that checks its own drafts},
  url    = {https://ai.thesatyajit.com/articles/flash-dllm},
  year   = {2026}
}
share