~/satyajit

LeapQuant: an 8-bit recurrent state that stops rounding every token

mdjsonmcp

2026-11-12 · 27 min · linear-attention · quantization · kernels · inference · vllm · state-space-models

A 5:01 narrated explainer, drawn in code. Every number and picture in it is this article's own; the sources are below.

› transcript

Store a linear attention state in BF16, and Qwen three point five, nine B, falls from eighty-seven point nine to seventy-two point one on a math benchmark. Why? Hi, I'm Ludo! LeapQuant stores this state in eight bits and keeps full precision accuracy. Here is how. Chapter one. A state read every token. Each Gated DeltaNet head keeps one matrix, a hundred and twenty-eight square. A new token brings a key, a correction and a decay. The update decays the matrix, then adds one rank-one write: the key, times what the memory got wrong about that key. That is the delta rule. Then the whole matrix goes back to memory. Every token, every head, every layer. On Qwen three point eight, twenty-seven B, that is a hundred and forty-four mebibytes per stream. Halve the bytes and you halve the bill. Here is the formula. First the decay. A slow head forgets a tiny fraction per token. Then one rank-one write. Each write is small next to the state it lands on. Chapter two. Why rounding piles up. Follow one entry of the state as it slowly decays. The faint lines are the only values a low-bit format can store. Round it after every token. Each change is smaller than half a level, so it rounds straight back, and the stored value never moves. In BF16, a decay of zero point nine nine nine is erased completely. LeapQuant holds the stored state fixed, buffers sixteen tokens of updates in higher precision, and applies them exactly. It rounds once, at the window's end. A few roundings instead of one per token, and nothing small is thrown away. The paper measured this on Qwen three point five over sixty-four thousand tokens. Rounded every token, INT8 and BF16 both drift away. Rounded once per window, the error stays near the floor, and LeapQuant's line is lowest of all. Chapter three. Lift the outliers, smooth the rest. Here is one value channel. Its scale is set by its largest entry, so two outlier rows leave ordinary entries on a grid far too coarse. LeapQuant subtracts the strongest rank-one pieces. Each has the shape of a token's write, so it rides in the window buffer, kept in FP16. Smoothing then divides each key row by the square root of its mean size, and multiplies back when it reads. On one real Qwen three point five head, the peak falls from a hundred and seventy-six times the median, to forty-seven, to five point three. The authors animate the same idea. Outlier rows and columns lift out as Compensator Tokens, and the rest is rescaled row by row until it is flat. The paper's picture of a window: residual, plus Compensator Tokens, plus buffered real tokens, add up to the full state. The compensators sit in the same list as real tokens, so the kernel needs no second path. A window starts from the stored state: an INT8 residual, its scales and four FP16 tokens. One point one nine bytes per value, against four. Each decode step reads that state and the buffer, and appends its own key and correction. It writes no state. After sixteen tokens, the kernel rebuilds the full state. It fits four new Compensator Tokens, subtracts them, and smooths the rows. Then it rounds once, and the next window begins. Chapter four. Does it hold up? The paper's ablation. INT8 rounded every token scores seven point one. The window lifts it to eighty-two point four, compensators to eighty-six point six, and smoothing to full precision's eighty-seven point nine. Kernel throughput for one layer. At batch five hundred and twelve, Gated DeltaNet runs two point six eight times faster on a B200. And four point two five times on an RTX fifty ninety, the best cell. End to end, the paper averages one point five seven times. The open code is narrower than the paper. It is eight-bit only. It writes the low-bit state inside the full precision slot, so it saves traffic, not memory. It refuses prefix caching, which is where the paper measured its memory savings. And its kernels use copy engines that arrived with Hopper, so an RTX thirty ninety is out. A low-bit recurrent state works, if you round it rarely, and make each rounding small. Round rarely, lift the outliers, smooth the rest. See you next time! Every source is in the full article. I'm Ludo. Bye!

Why read this

Hightop 30%

Delta rule and rounding-error recurrence from first principles, a simulator of all three fixes, and a code read separating what ships from what was measured.

  • Interactive explanations
  • Original analysis
  • Widely used

Quantization & compressionRuns on a consumer GPUApache-2.0Practitioner paper

How this was scored
Is it new?
2 of 3: A real new idea, method or capability
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
1 of 3: API-only, gated or restrictive licence
Will I understand it?
3 of 3: Mechanism carried by interactives built from real code or data
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
2 of 3: A reference for a year or more
Does it affect many?
2 of 3: A widely used model, tool or lab release
Only here?
2 of 3: A teardown or measurement few others did

Score 71 of 100, ranked 118 of 740 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

Earlier today I worked out why Qwen3.8-27B serves only so many streams on a 3090. The answer was not the KV cache. Each stream carries 48 Gated DeltaNet layers of recurrent state, 144 MiB of it in the FP32 the config asks for, allocated before the first token and read and rewritten on every decode step. Halve the precision and you halve the bill. Nobody halves it, because the models get worse when you do.

LeapQuant (Yi Pan, Haocheng Xi and eleven co-authors from UC Berkeley, UW, MIT, Perplexity and NVIDIA; I read v2 of 10 November) is about that last sentence. Its claim, from Yi Pan's thread: 8-bit recurrent states that match FP32 accuracy across Qwen, Kimi and GLM models, linear-attention kernels up to 4.25x faster, up to 2x inference throughput and up to 56% less memory, all without training or calibration data. The code is at github.com/leapquant/leapquant under Apache-2.0, and there is a project page at leapquant.github.io.

The thread's 34-second animation explains the method well, and three clips from it appear below.

Rounding every token against rounding once per 16-token window. The trajectories are drawn, not measured (video from @conlesspan's post on X).

The state, from the update rule up

Softmax attention keeps every key and value and pays for it with a cache that grows with the context. Linear attention keeps one matrix per head (the linear-attention roundup reads that matrix as an online regressor; here it is enough to read it as a memory). With keys kt∈Rdkk_t \in \mathbb{R}^{d_k} and values vt∈Rdvv_t \in \mathbb{R}^{d_v}, the plainest version adds each pair into a state St∈Rdk×dvS_t \in \mathbb{R}^{d_k \times d_v} and reads it with the query:

St=St−1+ktvt⊤,ot=St⊤qt.S_t = S_{t-1} + k_t v_t^{\top}, \qquad o_t = S_t^{\top} q_t .

Read SS as an associative memory: query it with a stored key and you get that key's value plus crosstalk from everything else. Nothing is ever removed, so the crosstalk grows. The delta rule (Schlag et al., the "fast weight programmers" paper) fixes that by first asking the memory what it currently returns for ktk_t, and writing only the difference:

St=St−1+βt kt(vt−St−1⊤kt)⊤=(I−βtktkt⊤)St−1+βtktvt⊤.S_t = S_{t-1} + \beta_t\, k_t \bigl(v_t - S_{t-1}^{\top} k_t\bigr)^{\top} = \bigl(I - \beta_t k_t k_t^{\top}\bigr) S_{t-1} + \beta_t k_t v_t^{\top}.

βt∈(0,1)\beta_t \in (0, 1) is a learned write strength. With βt=1\beta_t = 1 and a unit-norm key, the old association along ktk_t is erased and replaced. The Gated DeltaNet paper writes the second form in the transposed dv×dkd_v \times d_k orientation, as St−1(I−βkk⊤)+βvk⊤S_{t-1}(I - \beta k k^\top) + \beta v k^\top. Same thing. I use the paper's dk×dvd_k \times d_v orientation throughout, where keys index rows and values index columns.

Gated DeltaNet (GDN, used by Qwen3.5 and Qwen3.8-Flash-Next) adds a forgetting gate, a scalar decay αt∈(0,1)\alpha_t \in (0, 1) per head per token. Kimi Delta Attention (KDA, used by Kimi Linear and GLM-5.3-Flash) makes the decay a vector, one factor per key channel, which gives each channel its own half-life. The LeapQuant paper writes both as one update, its equation 1:

St=Diag⁡(αt) St−1+βt kt(vt−St−1⊤(αt⊙kt))⊤,ot=St⊤qt.S_t = \operatorname{Diag}(\alpha_t)\, S_{t-1} + \beta_t\, k_t \bigl(v_t - S_{t-1}^{\top}(\alpha_t \odot k_t)\bigr)^{\top}, \qquad o_t = S_t^{\top} q_t .

Expand KDA's (I−βkk⊤)Diag⁡(α)S+βkv⊤(I - \beta k k^\top)\operatorname{Diag}(\alpha) S + \beta k v^\top and you get exactly this (the paper's Appendix A does it for both). The form that matters for everything below is its equation 3. Call the bracketed term the correction, ut=βt(vt−St−1⊤(αt⊙kt))u_t = \beta_t\bigl(v_t - S_{t-1}^{\top}(\alpha_t \odot k_t)\bigr), and every token's update is a decay followed by one rank-one write:

St=Diag⁡(αt) St−1+ktut⊤.S_t = \operatorname{Diag}(\alpha_t)\, S_{t-1} + k_t u_t^{\top}.

A token is three small vectors, (αt,kt,ut)(\alpha_t, k_t, u_t). The state is a 128×128128 \times 128 matrix per head. Hold that asymmetry in mind.

Why a decode step is a memory copy

At decode, every linear-attention head does this per token: load SS from HBM, compute utu_t with one matrix-vector product, apply the decay and the rank-one write, compute oto_t with another matrix-vector product, store SS. For a 128×128128 \times 128 FP32 head that is 64 KiB in and 64 KiB out for roughly four multiply-adds per element, about one FLOP per byte. The ReplaySSM piece does the tighter count, 0.63 FLOP per byte, against the roughly 20 an H100 needs to keep its FP32 cores busy. Batching does not help, because every sequence brings its own state. The recurrence is a bandwidth problem with a little arithmetic attached.

The paper's Figure 1 puts a size on it. On GLM-5.3-Flash in NVFP4 at batch 256 on a B200, linear attention is about a third of decode time, and that share does not shrink as the context grows, because the state does not grow with the context. The right panel is the other half of the bill: with prefix caching, a serving engine keeps one copy of the state per cached prefix block, and on Qwen3.5-9B that overtakes the KV cache.

Left: stacked bars of decode-time share for GLM-5.3-Flash-NVFP4 at context lengths 4K to 256K, with linear attention near 34% at every length, GEMM around 50%, sparse attention growing from 3% to 12%. Right: horizontal bars of GPU memory for Qwen3.5-9B with prefix caching at 256K to 2M cached tokens, where the linear state cache grows to roughly 105 GB at 2M, more than the KV cache.
Where the recurrent state costs: (a) decode-time breakdown for GLM-5.3-Flash-NVFP4 on a B200 at batch 256; (b) memory for Qwen3.5-9B with one state checkpoint per 1,024 cached tokens (LeapQuant paper, Figure 1).

So the fix is obvious: store SS in fewer bits. vLLM and SGLang already let you pick BF16 instead of FP32. The paper's Table 1 shows what that costs.

Rounding every token

The naive scheme stores the state in a low-bit format and, after every decode step, computes the new state from the stored one and stores it again (the paper's equation 2):

St=Diag⁡(αt) S^t−1+ktut⊤,S^t=quantb(St).S_t = \operatorname{Diag}(\alpha_t)\, \hat S_{t-1} + k_t u_t^{\top}, \qquad \hat S_t = \mathtt{quant}_b(S_t).

Run an exact FP32 trajectory beside it, on the same inputs, and call the gap EtE_t. Subtract the two updates and vtv_t cancels. Appendix B.1 gets the error recurrence, its equation 9:

Et=Diag⁡(αt) Et−1−wt(Et−1⊤bt)⊤+εt,E_t = \operatorname{Diag}(\alpha_t)\, E_{t-1} - w_t \bigl(E_{t-1}^{\top} b_t\bigr)^{\top} + \varepsilon_t ,

with wt=βtktw_t = \beta_t k_t, bt=αt⊙ktb_t = \alpha_t \odot k_t, and εt\varepsilon_t the rounding error of step tt. Two readings of this. First, the error is not amplified: the decay and the delta-rule erase both shrink it, so it does not blow up exponentially. Second, nothing removes it either. Each εt\varepsilon_t is carried forward by exactly the dynamics that carry a memory written at step tt. A head whose decay is close to 1 remembers for thousands of tokens, and it remembers its rounding errors for just as long. The error settles where the rate at which rounding adds it balances the rate at which forgetting removes it. Round every token and that equilibrium sits high.

That would be tolerable if the per-step errors were independent noise. They are not, and BF16 shows why with no model involved. BF16 has 8 significant bits, so two neighbouring values are between 0.39% and 0.78% apart. Multiply a stored value by a decay of 0.999 and it moves by 0.1%, less than half the gap, so it rounds straight back to where it was. I checked this exhaustively over values from 0.01 to 10 with the simulator's BF16 rounding: with α=0.999\alpha = 0.999, 100% of stored values never decay at all. At 0.998 it is 96%, at 0.997 37%. A head that the model trained to forget slowly, stored in BF16 and re-rounded each step, does not forget. The same logic eats any part of a write ktut⊤k_t u_t^\top smaller than half a step. That is the paper's point in B.1: "each update is small relative to the state", so per-step rounding throws away part of it every time, in the same direction, and the errors add up coherently instead of averaging out. FP16, with three more mantissa bits, has the same cliff further out: my count puts 0.9995 safe and 0.9999 frozen.

Table 1 is what that does to reasoning. Over AIME 2026, BeyondAIME, GPQA-Diamond and LiveCodeBench v6 on three models, the FP32 state averages 84.0 and a BF16 state 72.5, the 11.5-point drop the thread quotes. On Qwen3.5-9B alone, AIME falls from 87.9 to 72.1 in BF16, to 3.9 in INT8 and to 0.0 in FP8. Its AIME traces run to tens of thousands of tokens, so there is a lot of time to drift. The collapse is model-specific: GLM-5.3-Flash keeps 87.4 on AIME with a per-step INT8 state, and Qwen3.8-Flash-Next is between the two. The paper's summary is that every 8-bit baseline still loses more than 10 points on at least one task of the two less sensitive models.

Figure 3 measures the drift directly, as state error against the FP32 trajectory over 64K decoded tokens of PG-19 on Qwen3.5-9B. INT8 per-step leaves the chart almost at once. BF16 per-step climbs steadily. The paper also tried stochastic rounding, the textbook cure for coherent rounding bias: it lowers the BF16 error at 64K by 3x and makes INT8 diverge.

Line chart of state MSE times ten to the minus five against decoded tokens from 0 to 64K. INT8 rounded every step shoots past 50 within about 4K tokens; BF16 rounded every step passes 50 by 16K. Dashed INT8-w rises slowly to about 16 at 64K. Dashed BF16-w and the LeapQuant line stay near zero; an inset shows LeapQuant below BF16-w, about 0.9 against 1.3 at 64K.
State error against the FP32 trajectory over 64K decoded tokens on Qwen3.5-9B. Solid lines re-quantize every token; '-w' lines once per 16-token window; LeapQuant is INT8 with all three fixes (LeapQuant paper, Figure 3).

Fix one: leap over a window

The error recurrence says the problem is how often you round, so round less often. That needs a way to compute outputs from a state you do not rewrite. Equation 3 gives it. Start a window at a quantized state S^0\hat S_0, apply pp tokens, and unroll (the paper's equation 4, with Γa:b\Gamma_{a:b} the product of the decays from step aa to step bb):

Sℓ=Γ1:ℓ S^0+∑j=1ℓΓj+1:ℓ kjuj⊤,1≤ℓ≤p.S_\ell = \Gamma_{1:\ell}\, \hat S_0 + \sum_{j=1}^{\ell} \Gamma_{j+1:\ell}\, k_j u_j^{\top}, \qquad 1 \le \ell \le p .

The state at any point in the window is the fixed boundary state, decayed, plus the window's rank-one writes, decayed. You never need SℓS_\ell itself, only Sℓ⊤xS_\ell^\top x for a query or a key. Transpose and multiply (Appendix B.2, equation 10):

Sℓ⊤x=S^0⊤ Γ1:ℓ x+∑j=1ℓuj(kj⊤Γj+1:ℓ x).S_\ell^{\top} x = \hat S_0^{\top}\, \Gamma_{1:\ell}\, x + \sum_{j=1}^{\ell} u_j \bigl(k_j^{\top} \Gamma_{j+1:\ell}\, x\bigr).

The first term is one pass over the stored low-bit matrix. Each buffered token adds one dot product and one scaled vector, O(dk+dv)O(d_k + d_v). The decays are diagonal and accumulate incrementally. So a decode step inside the window reads S^0\hat S_0, reads the buffer, appends its own (α,k,u)(\alpha, k, u) to the buffer, and writes no state at all. Only at the end of the window does the kernel materialize SpS_p and quantize it once, into the next window's S^0\hat S_0. Rounding happens pp times less often, and because what gets rounded is the accumulated update rather than sixteen separate small ones, the sub-half-step writes are no longer thrown away.

Two panels. (a) Per-token quantization: an FP32 state S0 is quantized to a blue S-hat-0, updated to S1 and quantized again, repeatedly, with errors labelled 0.5%, 2%, 10%. (b) Per-window quantization: S0 is quantized once to S-hat-0; buffer tokens 1 to p are added to it without re-quantizing, and only Sp is quantized, with errors labelled 0.5% and 1%.
Per-token against per-window quantization. The error percentages in the panels are schematic (LeapQuant paper, Figure 2).

This is the same identity as ReplaySSM, from Tri Dao's lab and NVIDIA, which this site covered in October and which LeapQuant cites. ReplaySSM also writes its state once per 16 tokens, to save the write traffic, and keeps the checkpoint at high precision. LeapQuant keeps only the low-bit copy and pays for it with one rounding per window, the "one checkpoint, rounded once per window" case that the ReplaySSM piece measured on a toy as better than per-step but far from free. That is why LeapQuant needs the next two fixes and ReplaySSM does not.

The ablation in Table 2 shows the window alone does most of the work. On Qwen3.5-9B, an INT8 state rounded every step scores 7.1 on AIME and 9.2 on LiveCodeBench; per-window INT8 scores 82.4 and 60.6. Per-window BF16 scores 87.8 on AIME, against FP32's 87.9. Note that Table 2's per-step INT8 run is not the one in Table 1, which reports 3.9 and 10.7 for the same cells; both are near zero. Table 3 varies the window: accuracy improves up to 16 tokens and stops, and the kernel is fastest at 16 too (2.52x over FP32 at batch 256 on a B200, against 1.59x at 4 tokens and 2.20x at 32), because a longer window means a longer buffer to read every step.

Here is all of this on a toy head you can drive. It is one GDN head with a 32×3232 \times 32 state, L2-normalised keys with two biased channels and four wide ones, two large biased value channels, decays just under 1 and write strengths of 0.05 to 0.15, run 4,096 steps exactly and again with a quantized state. The grey line stores every step with nothing else. The orange line uses your settings.

With the defaults (INT8, long memory), storing every step ends at 15% relative state error after 4,096 quantizations. A 16-token window with no other fix brings that to 4.3% with 256 quantizations. Four Compensator Tokens and smoothing bring it to 1.0%. At INT4 the difference is starker: per-step INT4 diverges, its error past 1000% by the end, while the window with four Compensator Tokens and smoothing holds near 21%. The toy saturates where Figure 3 keeps climbing, because my longest decay is shorter than a real model's; the order of the curves carries over, not their values.

Fix two: pull the outliers out as tokens

The window decides how often the state is rounded. The second fix decides how much each rounding costs. The paper's Figure 6 shows one Qwen3.5-9B head (layer 13, head 16) at 8K tokens of context, its entries divided by the median magnitude. The largest is 176 times the median, and the big ones sit in a few rows and columns.

With symmetric INT8 and one scale per value channel, which is what the code does, a column's step is its largest entry divided by 127. A column containing a 176x outlier quantizes every ordinary entry in it on a grid sized for the outlier: a median-sized entry gets less than one level. That is the source of the per-quantization error.

The structure is also low-rank. Figure 5 in the paper shows a few singular values carry most of the state's energy, and a value that is large along a row and a column at once is what a rank-one outer product looks like. So before quantizing SpS_p, fit the best rank-one approximation and subtract it (equation 5):

(k~,u~)=arg⁡min⁡k,u∥Sp−ku⊤∥F,Rp=Sp−k~u~⊤,R^p=quantb(Rp).(\tilde k, \tilde u) = \arg\min_{k, u} \bigl\lVert S_p - k u^{\top} \bigr\rVert_F, \qquad R_p = S_p - \tilde k \tilde u^{\top}, \qquad \hat R_p = \mathtt{quant}_b(R_p).

By Eckart and Young the minimiser is the top singular pair, and with rr of them it is the top rr. Only the residual RpR_p is quantized; the pairs are kept in FP16.

The name comes from the shape. k~u~⊤\tilde k \tilde u^\top is exactly the form of a token's write kjuj⊤k_j u_j^\top, so at the start of the next window the Compensator Tokens go into the window's buffer ahead of the real tokens, with a decay of 1. The readout in equation 10 already handles buffered rank-one writes, so the decode kernel processes them on the same path at the cost of rr extra records. The kernel docstring in kernels.py says it plainly: "the Compensator Tokens are handled exactly like R extra buffer entries with weight 1." No model-specific code, no second state.

Matrix diagram: an FP32 state Sp with one tall outlier column on top equals a flatter quantized residual R-hat-0, plus a purple rank-one Compensator Token k-tilde u-tilde transpose, plus grey buffer tokens k1 u1 transpose through kp up transpose, all bracketed as the reconstruction.
Per-window reconstruction with Compensator Tokens: the state is a low-bit residual, a few FP16 rank-one terms that hold its large-magnitude structure, and the window's buffered real-token updates (LeapQuant paper, Figure 4).

The paper uses r=4r = 4 at 8 bits and r=6r = 6 at 6 and 4 bits. Table 3 sweeps it at 8 bits: two tokens give 85.7 on AIME, four give 87.9, eight 88.1, and sixteen give 87.9 with the kernel at 0.74x, slower than FP32. Up to four are hidden behind the state read on a B200; more become visible. In the ablation, adding four Compensator Tokens to per-window INT8 lifts AIME from 82.4 to 86.6 and LiveCodeBench from 60.6 to 61.5.

How the fit is computed in the shipped kernel is cheaper than the argmin suggests. The paper says power iteration. make_flush runs ITERS=1 round of subspace iteration from a fixed random start (Q0, seeded with 0), orthogonalises with CholeskyQR2 on the tensor cores, and carries the previous window's factors algebraically instead of rebuilding them into the dense state. One iteration from a random start does not find the exact top-four subspace. It finds a subspace that holds most of a spectrum this concentrated, and what it misses lands in the residual, which is quantized anyway. That is a sound trade, and the accuracy tables are measured with it.

Fix three: smooth what is left

After the Compensator Tokens the residual is flatter, but not flat. In Figure 6 the peak drops from 176x the median to 47x; a few key rows still stand out. Per-value-channel scales do nothing about a row that is large across every column. So the third fix rescales rows. For each key row ii of the residual, take cic_i as the square root of the row's mean absolute value, divide the row by it, and quantize (equation 7):

R^0C=quantb(C−1R0),C=Diag⁡(c).\hat R_0^{C} = \mathtt{quant}_b\bigl(C^{-1} R_0\bigr), \qquad C = \operatorname{Diag}(c).

The square root splits the difference between doing nothing and fully normalising, the same move SmoothQuant makes between activations and weights. The scales are FP32, fixed within a window and recomputed at each boundary. Undoing it costs nothing at read time. Appendix B.3 writes the dequantized INT8 payload as (Z0/127)B(Z_0 / 127) B, with BB the per-value-channel scales, so

[C dequant(R^0C)]⊤Γx=B Z0⊤127 C Γx,\bigl[C\,\mathtt{dequant}(\hat R_0^{C})\bigr]^{\top} \Gamma x = B\, \frac{Z_0^{\top}}{127}\, C\, \Gamma x ,

and CC folds into the vector Γx\Gamma x before it meets the integer matrix. The kernel multiplies a 128-long vector by one more diagonal. The code computes exactly the paper's rule, sk = torch.sqrt(res.abs().mean(-2)).clamp_min(1e-12) in the KDA path and s_k[c] = sqrt(mean_v |E[v, c]|) in the flush kernel's docstring.

In Figure 6 smoothing takes the peak from 47x to 5.3x the median. In the ablation it takes per-window INT8 with Compensator Tokens from 86.6 to 87.9 on AIME and from 61.5 to 64.1 on LiveCodeBench, both equal to FP32, at a kernel speedup of 2.52x.

Three 3D surface plots of magnitude over median across 128 value channels and 128 key channels. (a) The original state, labelled hard to quantize, has red spikes up to 176 concentrated in a band of value channels. (b) After Compensator Tokens, labelled easier, most spikes are gone but a few reach 30. (c) After smoothing, labelled easy, the surface is flat and low.
One Qwen3.5-9B head (layer 13, head 16) at 8K context, normalised by the median: outliers reach 176x in the state, 47x after Compensator Tokens, 5.3x after smoothing (LeapQuant paper, Figure 6).
Outliers lifted out as Compensator Tokens, the residual smoothed, then the measured error curves. The intermediate peak ratios the animation counts through are in-between frames; the paper's numbers are 176x, 47x and 5.3x (video from @conlesspan's post on X).

In my toy the order is the same and the proportions are not. The exact state's peak is 48x its median, four Compensator Tokens take the residual to 5.3x, and smoothing to 5.1x. My outlier rows are mostly rank-one, so the Compensator Tokens remove them and leave smoothing little to do. Turn the Compensator Tokens off in the simulator and smoothing alone is worth something: per-window INT8 goes from 4.3% to 3.0%. The paper's head has row structure that four rank-one terms do not capture, which is why smoothing earns its 2.6 points on LiveCodeBench there.

What 1.19 bytes buys, and what it costs to carry

The paper says each window-boundary state takes 1.19 bytes per element instead of 4, a 3.4x reduction. The layout comment in state.py gives the pieces, and the arithmetic per 128×128128 \times 128 head is mine: 16,384 bytes of INT8 residual, 128 FP32 smoothing scales for the key rows and 128 FP32 quantization scales for the value channels (1,024 bytes), and four Compensator Tokens of 128 + 128 FP16 values (2,048 bytes). That is 19,456 bytes, 1.1875 bytes per element. It checks.

That figure is for a state at rest, a checkpoint. A request that is decoding also carries its window buffer: 16 tokens of key and correction, 8 KiB per head, which matches the paper's "around 8 KiB per head per layer" (the paper says FP16; the code's Kbuf and Ubuf are BF16, the same size). That adds half a byte per element, so an active GDN stream holds 1.69 bytes per element. KDA's buffer also keeps a per-channel FP16 decay for each buffered token (the Gbuf tensor), another 4 KiB per head, so an active KDA stream holds 1.94, barely under BF16's 2. The memory win on a live request is modest. The big one is on cached checkpoints, which carry no buffer, and that is where the paper measures it.

Traffic is the better story. An FP32 state moves 8 bytes per element per step, 4 in and 4 out. On my reading of the kernels, a LeapQuant step reads the 1.19-byte checkpoint and the buffer (half full on average), appends one record, and writes a checkpoint once every 16 steps, about 1.5 bytes per element per step for GDN. That is roughly 5x less than FP32 and 2.6x less than BF16, which is in the range of the kernel speedups the paper measures. This is an estimate from the layout, not a measurement.

For the Qwen3.8-27B case that started this, 48 layers of 48 heads, the state per stream would go from 144 MiB in FP32 to 60.75 MiB active or 42.75 MiB as a cached checkpoint. The 3090 article inferred a pool of five to six GiB from HyperQwen's logs, where 37 FP32 streams actually ran. Taking 5.5 GiB as the midpoint, that is 39 FP32 streams, 78 in BF16 and 92 with LeapQuant's layout. The last number is what the paper's design would give. The next section is about why the released code does not give it yet, and why it would not run on a 3090 anyway.

The speed numbers, cell by cell

Figure 7 is one linear-attention layer (32 heads, dk=dv=128d_k = d_v = 128) against the FP32 kernel in FLA. The 4.25x in the thread is one cell: GDN on an RTX 5090 at batch 512. At batch 512, GDN is 2.68x, 3.95x and 4.25x faster on the B200, RTX PRO 6000 and RTX 5090, and KDA 2.41x, 3.38x and 3.64x. The abstract's "2.05-3.70x" is the average per GPU. The gains are larger on the two workstation cards, which the paper reads as state traffic being a stronger bottleneck there.

Six bar-chart panels of throughput in millions of tokens per second against batch sizes 32, 128 and 512, FP32 against LeapQuant, for GDN and KDA on B200, RTX PRO 6000 and RTX 5090. Speedups printed above the bars run from 1.33x to 4.25x, rising with batch size; the largest is GDN on RTX 5090 at batch 512.
Kernel throughput of one linear-attention layer, FP32 against LeapQuant INT8 (LeapQuant paper, Figure 7).

A layer kernel is not a model. In pure decode steps through vLLM at 4K context, the gain is 1.22-1.29x at the largest batch on a B200 and 1.22-1.57x on the RTX PRO 6000, growing with batch size as linear attention takes more of each step. Appendix C has the gain still at 1.18x for Kimi-Linear-48B-A3B at 8K context.

End to end (Figure 9), the paper reports 1.19-2.98x output throughput on the B200 and 1.31-1.65x on the RTX PRO 6000, and 1.57x on average. The top of that range is GPQA, whose long outputs make the state checkpoints dominate memory: concurrency rises from 10 to 23 requests per GPU on GLM-5.3-Flash and from 32 to 61 on Qwen3.8-Flash-Next. The thread's "up to 2x" undersells the paper's own maximum and oversells its average. Those end-to-end runs keep a state checkpoint every 1K cached tokens, which the paper calls the vLLM and SGLang default with prefix caching on, and that is also the setting for the memory figure: 41%, 51% and 56% less end-to-end memory on Qwen3.5-9B, Qwen3.5-35B-A3B and Kimi-Linear-48B-A3B. The spacing matters, because the saving scales with how many checkpoints are kept. The FlashState paper found vLLM checkpointing GLM-5.3-Flash only at 4,352-token block boundaries and SGLang on a 256-token grid, so real engines sit on both sides of the paper's 1K.

The accuracy and kernel numbers from Table 2 and Figure 7 as the animation shows them; the bars grow through in-between values before landing (video from @conlesspan's post on X).

The accuracy side holds up at 8 bits. LeapQuant averages 84.4 over the twelve model-task pairs against FP32's 84.0 and stays within 2.5 points of FP32 on every pair. At 6 bits it also averages 84.4. At 4 bits it averages 80.6 against TurboQuant's 45.0 and MXFP4's 16.7, within 3.2 points on Qwen3.5-9B and 1.3 on Qwen3.8-Flash-Next, but GLM-5.3-Flash loses 6.5 points on AIME and 11.7 on BeyondAIME. "Matches FP32" is an 8-bit and 6-bit claim.

What the released code does

I cloned the repository at commit 61434a9 (11 November) and read state.py, the two vLLM hooks and the parts of kernels.py (2,332 lines of TileLang) I cite: the docstrings, the step kernel's load stage and the flush kernel's fit and residual. I did not run it: there is no Hopper or Blackwell GPU here. What the code says:

One comment in the flush kernel is the kind you only write after a bad night: a floor on the cumulative decay because "a fully decayed head has a denormal product (2e-41 seen on 9B layer 11) and 1/pn = inf", which poisoned the residual into NaN outputs. Carrying a product of 16 decays and dividing by it is exactly where a real model's gates will find the edge of FP32.

Worth running on Blackwell, not yet on a 3090

The analysis is the best part. Equation 9 plus the observation that per-step rounding is biased, not noisy, explains both the BF16 drop and why stochastic rounding barely helps, and the fix follows from it: round less often, and make each rounding cheaper. The window is the big lever, worth about 75 AIME points over per-step INT8 on Qwen3.5-9B by itself; Compensator Tokens and smoothing take the last five. Reusing the token path for the outlier factors is the kind of design where the kernel gets simpler instead of harder.

For an engine that serves GDN or KDA models on Blackwell, at large batch, without prefix caching, the released plugin is worth trying today: the kernel speedups come from traffic and the plugin keeps that. Everyone else should read the paper for the method and wait for the port. The memory story, which is the one the 3090 case needed, is in the paper and not yet in the code. And if you are on a consumer card doing what HyperQwen did, storing the state at 16 bits and re-rounding it every token, the BF16 arithmetic above is a reason to prefer FP16 over BF16: FP16's mantissa still moves a 0.9995 decay that BF16 cannot represent. That last point is my reasoning from the number formats; the paper tests BF16 and not FP16.

The step I would watch for next is the one ReplaySSM already took: keep the boundary state in high precision for the recurrence and use the low-bit copy only for reads. That takes most of the rounding out of the loop, though not all: the high-precision copy is itself rounded at each flush unless it stays in FP32, and on GDN the corrections computed from the low-bit copy carry its error through the rest of a window. It also costs the memory LeapQuant is trying to save. LeapQuant's answer is that with three fixes, one rounding per 16 tokens is small enough. On its tables, at 8 bits, it is.

How I checked. Read: the paper (v2), its appendices and all six figures I reproduce; the thread and its video; state.py, vllm/gdn.py, vllm/glm.py, serve.sh and pyproject.toml in full and the cited parts of kernels.py, at commit 61434a9. Computed: the 1.1875 bytes per element and 8 KiB buffer from the layout, the per-stream sizes from the Qwen3.8-27B and GLM-5.3-Flash shapes, the traffic estimate, and the BF16 and FP16 decay counts. Ran: the toy simulator above, whose code is components/articles/leapquant/sim.ts. Not run: LeapQuant itself.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "LeapQuant: an 8-bit recurrent state that stops rounding every token", ai.satyajit.si, November 2026.

bibtex
@misc{ghana2026leapquant,
  author = {Satyajit Ghana},
  title  = {LeapQuant: an 8-bit recurrent state that stops rounding every token},
  url    = {https://ai.satyajit.si/articles/leapquant},
  year   = {2026}
}
share