This section is about computing a linear recurrence fast on a GPU, during training, forward and backward. Every layer in this chapter keeps a state and updates it the same way. At each step it multiplies the old state by a decay, a number that says how much of it survives, and adds something new. Run as a loop over T tokens, that is T steps in a row, each waiting for the one before, while thousands of GPU cores wait with it. A layer that cannot train fast on long sequences is never shipped, however cheap it is at decoding, so this is where recurrent layers win or lose against attention.
§21.1 showed the way out in principle. Because each update is linear in the state, any stretch of steps can be summarized by one update of the same form. A parallel scan combines those summaries in about log2T rounds instead of T. This section turns that idea into what production kernels actually run, for the vector and matrix states that real layers carry.
One hardware fact shapes everything that follows. A GPU has two kinds of arithmetic unit. Its tensor cores do nothing but multiply small matrices, and on an H100 they do it at about 989 TFLOP/s in bf16. Its ordinary float32 units do everything else at about 67 TFLOP/s, nearly 15 times slower (§11.1). A scan is elementwise multiplies and adds, so it runs on the slow units however cleverly it is arranged. An algorithm that does twice the arithmetic as matrix products can therefore win easily. That is what happened in 2024. A chunked algorithm that does more arithmetic than Mamba's carefully fused scan (§21.4) ran 2 to 8 times faster than it at a state size of 64 (Dao & Gu 2024).
We get there in steps. First we extend the scan from one number of state to vectors and matrices, and count what each step of it costs. Then we derive the backward pass, which is another linear recurrence, run backward in time. Then we see why long products of decays break in float32 and how kernels avoid it. Then we derive, prove, and size the chunkwise algorithm, which splits the sequence into short chunks and does most of its work as matrix products. The section ends with when a scan is the wrong tool, and how the same idea splits one sequence across several GPUs.
The scan for one number of state
Start with the smallest case, a state that is one number. At each step the state is the old state times a decay, plus a new input,
ht=atht−1+bt,t=1,…,T.
Here at is the decay, the fraction of the old state that survives step t, and bt is the input written at step t. The state starts at h0=0 unless we say otherwise. This is the scalar linear recurrence on which §21.1 builds the chapter.
Each step is a rule of the form "multiply by at, then add bt." A function of this form, h↦ath+bt, is called an affine map, and the pair (at,bt) describes it completely. The fact that makes everything work is that two such steps in a row are again one such step. Take "multiply by 0.5 and add 1" followed by "multiply by 0.5 and add 0." Starting from any h, that gives 0.5(0.5h+1)+0=0.25h+0.5, which is the single step (0.25,0.5).
In general, applying (a1,b1) and then (a2,b2) gives a2(a1h+b1)+b2=a1a2h+(a2b1+b2). §11.4 wrote this as an operator on pairs,
(a1,b1)∙(a2,b2)=(a1a2,a2b1+b2),
whose identity is (1,0), the step that changes nothing.
In plain words, combining two steps multiplies their decays and carries the first step's input through the second step's decay.
The operator is associative, which means (p1∙p2)∙p3=p1∙(p2∙p3) for any three steps. So a long chain of steps can be combined in any grouping, and different processors can combine different parts of it at the same time. That freedom is what allows a parallel computation.
Call one application of ∙ to two pairs a combine. A parallel algorithm is measured by two numbers. Its work is the total number of combines it does. Its depth is the number of rounds that must run one after another. A plain loop has work T−1 and depth T−1. Two parallel scans matter here, and both come back later.
In the first, every element combines with the element 1 place to its left, then with the one 2 places to its left, then 4, and so on, each round using the values from the round before. After ⌈log2T⌉ rounds every position holds its full prefix. The work is about Tlog2T combines. This is the Hillis-Steele scan (Hillis & Steele 1986).
The second sweeps up a binary tree, combining neighbors in pairs until the root holds the whole sequence. Then it sweeps back down, handing each node the combination of everything to its left. Its depth is 2log2T and its work is 2(T−1) combines, about twice the loop's. This is the Blelloch scan (Blelloch 1990).
Brent's theorem, also from §11.4, turns these counts into time. On p processors, a computation with work W and depth D finishes in at most W/p+D steps. So low depth helps only when there are enough processors to use it, a point that matters at the end of this section.
§11.4 stopped at a scalar state, h0=0, the forward pass, and float64 in NumPy. A real layer differs in five ways. It has a vector or matrix state. It starts from a nonzero state whenever a long sequence is processed in pieces. It needs a backward pass. It multiplies thousands of decays in float32. And it runs on a GPU whose tensor cores a scan never touches. Each of these changes the algorithm, and the rest of the section takes them one at a time.
Scans over vector and matrix states
Real layers keep many numbers of state, and in general a step may mix them. The old state vector is multiplied by a matrix, and then a new vector is added. The general linear recurrence has a state ht∈RN of N numbers, a transition matrixAt∈RN×N that decays and mixes the old state, and an input bt∈RN,
ht=Atht−1+bt,t=1,…,T.
Both At and bt may be computed from the layer's input, but never from the state. That is what keeps the recurrence linear in its state.
Two layers of this chapter show the range. In Mamba (§21.4), a recurrent layer whose decays are computed from each token, every channel keeps N state numbers, each with its own decay. So At is a diagonal matrix with the decays on its diagonal, and bt is the token's input written into the state.
In linear attention (§21.2), attention without the softmax rewritten as a recurrence, the state is a dk×dv matrix. Each token adds the outer productktvt⊤ of its key and value, the matrix whose entry (i,j) is kt,ivt,j. With a decay, the update is St=atSt−1+ktvt⊤. Each of the dv columns of St then follows the vector recurrence above, with the transition atI. So one definition covers every layer in the chapter, once the state is allowed to be a matrix.
The formula is the expansion A2(A1H+b1)+b2=(A2A1)H+(A2b1+b2). The later step's matrix stands on the left in both components. For scalars the order of a1a2 did not matter, which is why §11.4 could write it either way. For matrices it does.
In plain words, steps can be grouped in any way but never reordered. A parallel scan only regroups, which is why it is allowed. But every implementation must keep the earlier step on the correct side. In the Blelloch down-sweep, for example, the prefix arriving from the left must stay on the left of each combine, which is how §11.4 wrote it.
A nonzero initial state h0 costs nothing extra, and there are two ways to handle it. Write (At:0,βt) for the scan's output at position t, the composed map of steps 1 to t. Applied to h0 it gives ht=At:0h0+βt, so one option is to apply the prefixes to h0 at the end.
The other option is to fold h0 into the first step before scanning. Replace b1 by b1+A1h0, then scan as if h0=0. For a scalar with h0=2 and first step (0.5,1), the folded input is 1+0.5⋅2=2, which is indeed h1. Kernels use this second way. It is what lets a long sequence be processed in pieces, each piece starting from the state where the previous one ended.
What one combine costs
The work of a scan is the number of combines times the cost of one combine. The cost of one depends entirely on the structure of At.
This is why no practical model uses a dense transition computed from the input. With N=16, a dense combine costs about 8,700 FLOPs against 48 for a diagonal one, 180 times more. The layer would also have to produce N2 numbers per token and channel just to build At. Little is lost by avoiding it. §21.1 showed that a dense A that stays the same at every step can almost always be diagonalized over the complex numbers, which turns it into independent scalar recurrences that compute the same outputs.
Layers whose transitions change with the input use one of three structures instead.
A diagonal matrix, one decay per state entry. Mamba uses it, and so do gated linear attention (GLA), linear attention with one decay per key dimension (Yang et al. 2023), and the recurrent layer of Griffin (De et al. 2024).
A scalar times the identity, one decay for a whole head. Mamba-2 uses it (Dao & Gu 2024). RetNet uses the same structure with a fixed decay for each head, one that does not depend on the input (Sun et al. 2023).
The identity minus a rank-one matrix, used by the delta-rule layers of §21.6, which overwrite what the state stores under a key instead of only adding to it. These are harder to compose, because a product of k such matrices is the identity minus a matrix of rank up to k. §21.6 keeps those products cheap by storing them as two thin factors, the WY representation.
The last sentence of the box, one stored state per token, is the real obstacle for matrix states.
Keep that number in mind, because the chunkwise algorithm later in this section exists to avoid it. An interviewer who asks how to parallelize a recurrence over time wants both halves of the answer. The combine is cheap only for structured transitions, and a scan stores one state per token.
The cell checks associativity and non-commutativity for dense 3×3 steps. Then it runs the Hillis-Steele scan with matrix-affine elements and compares it with a loop. The only change from the scalar scan is the order of the matrix product.
Python · cell 1
Warm-up
Compose affine maps with matrices
Write two functions for steps p = (A, b), where A has shape (N, N) and b has shape (N,) or (N, M).
compose(p1, p2) returns the single step that applies p1 first and then p2.
compose_all(maps) composes a non-empty list of steps, the first applied first, as a balanced tree. In each round, combine the neighbors (0, 1), (2, 3), .... If one element is left over at the end of a round, carry it into the next round unchanged. Stop when one step remains.
This is how a reduction tree composes steps in logarithmic depth. It must give the same answer as composing left to right.
Tests, verified solutions, and AI help, open once you add your API key.
A vectorized scan for diagonal states
The common case is a diagonal transition, one decay per state entry. Write at∈RN for the diagonal and ⊙ for the elementwise product. The recurrence is ht=at⊙ht−1+bt, and the combine is
(a1,b1)∙(a2,b2)=(a1⊙a2,a2⊙b1+b2).
That is N independent scalar scans side by side. So one Hillis-Steele round is three whole-array operations on arrays of shape (T,N), and NumPy (or a GPU) does every channel and every position of the round at once.
The Blelloch scan works for these pairs too. It does 2(T−1) combines on a length padded to a power of two, instead of about Tlog2T. The cell implements it vectorized over channels and counts the combines. In the down-sweep the prefix is always the first argument of combine, so the non-commutative operator is applied in the right order.
Python · cell 2
Blelloch did 3,046 combines here. That is 1,023 on the way up, 1,023 on the way down, and 1,000 more to turn the exclusive scan into the inclusive one. Hillis-Steele did 8,977 and the loop 999. So the work-efficient scan costs about three times the loop's work, the price of depth 20 instead of 999.
Interview
A vectorized parallel scan for diagonal recurrences
Implement parallel_linear_scan(a, b, h0=None) for arrays a and b of shape (T, N). It computes every state of ht=at⊙ht−1+bt, starting from h0 of shape (N,), which means zeros when None.
Use a Hillis-Steele scan over the affine-pair operator.
Each round must be a few whole-array operations, with no Python loop over t.
Return (h, rounds), where h has shape (T, N) and rounds is the number of rounds you ran. It must equal ⌈log2T⌉, which is 0 for T=1.
Do not modify the caller's arrays.
import numpy as np
def parallel_linear_scan(a, b, h0=None):
a, b = np.asarray(a, dtype=float), np.asarray(b, dtype=float)
return np.cumsum(b, axis=0), 0 # placeholder: ignores every decay, and is not a scan of the pairs
Tests, verified solutions, and AI help, open once you add your API key.
The backward pass is a scan in reverse
Training needs the gradient of the loss with respect to every decay at, every input bt, and the initial state. The loss depends on all T states, and each parameter affects every later state. Tracing each parameter's effect on each later state separately would cost O(T2). Reverse-mode differentiation (§0.3) carries one running gradient backward through the computation instead, and does it in O(T). Here the running gradient obeys a recurrence of exactly the kind we already know how to scan.
Work with one scalar channel, since a diagonal state is N of them side by side. Let the loss L depend on the states h1,…,hT. Two kinds of derivative matter.
The direct gradientgt=∂L/∂ht holds the other states fixed. It is what the layer's output reports back. For an output yt=ctht, it is gt=ct∂L/∂yt.
The adjointhˉt=dL/dht is the total derivative. It counts every path from ht to the loss, including the paths through later states. It is the running gradient of §0.3.
In this section an overbar always means an adjoint, never a discretized parameter as in §21.3.
In plain words, the gradient flows backward through the same chain of decays, picking up each step's direct gradient on the way. Reverse time and shift the decays by one place, since the gradient passes from step t+1 back to step t through at+1. Then the adjoint recursion is a linear recurrence of exactly the forward kind, with inputs gt, and the same scan kernel computes it on reversed arrays. The decay gradient then needs one elementwise product with the previous states.
The theorem keeps its shape for larger states. For a diagonal state every quantity is a vector and every product is elementwise. For a dense transition, hˉt=gt+At+1⊤hˉt+1 and ∂L/∂At=hˉtht−1⊤, by the matrix calculus of §0.2.
The cell runs the four-step example, then checks all three gradients against finite differences on a random recurrence of 50 steps.
Python · cell 3
That leaves memory. The decay gradient hˉtht−1 needs the previous state at every step. Storing all of them costs TN numbers for every channel, the very tensor that Mamba's fused kernel (§21.4) refuses to write to HBM. So Mamba recomputes the states from the inputs during the backward pass. That is the same trade FlashAttention makes when it recomputes its attention matrix instead of storing it (§12.3). Chunked kernels store only the state at each chunk boundary and recompute the states inside a chunk from it. An interviewer who asks how recurrent layers train on long sequences without storing a state per token wants this answer.
Interview
Backpropagate through a linear recurrence
Implement linear_recurrence_backward(a, b, h0, g). The arrays a, b, g have shape (T, N) and h0 has shape (N,). The forward pass is ht=at⊙ht−1+bt, and the loss is L=∑t∑ngt,nht,n, so g is the direct gradient.
Run the forward pass yourself to get the states.
Compute the adjoints with the reverse recurrence. A loop over t is fine here, since the point is the recurrence, which a reverse scan would parallelize.
Return (da, db, dh0), with the shapes of a, b, and h0.
import numpy as np
def linear_recurrence_backward(a, b, h0, g):
g = np.asarray(g, dtype=float)
return np.zeros_like(g), g.copy(), np.zeros(g.shape[1]) # placeholder: db = g ignores the path through later states
Tests, verified solutions, and AI help, open once you add your API key.
The backward pass of linear attention
The matrix state of linear attention is worth doing in full, because the chunked kernels later in the section train exactly this layer. The forward pass starts from S0=0 and updates St=atSt−1+ktvt⊤. It then reads the output ot=St⊤qt with the query qt∈Rdk. Write oˉt=∂L/∂ot for the gradient arriving at each output.
The read ot=St⊤qt gives the state the direct gradient qtoˉt⊤. So the theorem gives the reverse recurrence
In plain words, the query gradient is the odd one out. A query is used once, in the read at its own step, so its gradient needs the forward state St and is computed in a forward sweep. A key, a value, or a decay is written into the state and reaches every read from its own step on. So its gradient goes through the adjoint state Sˉt, in a backward sweep. Each sweep holds one dk×dv state at a time.
The decay gradient aˉt=⟨Sˉt,St−1⟩ seems to need every forward state St−1 during the reverse sweep. Those are the T matrices that no kernel wants to write to HBM. In fact it needs none of them.
In plain words, the loss is a sum of contributions, one for each pair of a write at step s and a later read at step t≥s. Raising one log decay ℓu scales up exactly the contributions whose path crosses step u, those with s<u≤t. The number qt⊤qˉt is the sum of all contributions to the read at step t. The number ks⊤kˉs is the sum of all contributions from the write at step s. Summing the reads from u on and subtracting the writes from u on leaves exactly the paths that cross step u. A token's read of its own write appears in both sums and cancels, as it must, since that path is never decayed.
So the decay gradient costs about 4Tdk more operations on numbers we already have, and no states at all. Section 4.3 of Yang et al. (2023) gives this closed form for gated linear attention, which has a vector of decays per step, and notes that earlier work had assumed the states must be stored to compute it. Exercise 21.5f builds the whole backward pass this way.
∂ Proof that the decay gradient needs no states
Write wts=at:s(qt⊤ks)(vs⊤oˉt) for s≤t. It is the contribution of the write at step s to the read at step t, as seen by the loss.
By the chain rule, ∂L/∂ℓu=∑toˉt⊤∂ot/∂ℓu. That is the derivative of L~=∑toˉt⊤ot with the oˉt held fixed. By the unrolled state, L~=∑s≤twts.
The segment product at:s=eℓs+1+⋯+ℓt depends on ℓu only when s<u≤t, and then ∂at:s/∂ℓu=at:s. So ∂L~/∂ℓu is the sum of wts over the pairs with s<u≤t.
Those pairs are all pairs with t≥u, minus the pairs with s≥u (which have t≥s≥u automatically). By the unrolled gradients in the box above, ∑s≤twts=qt⊤qˉt and ∑t≥swts=ks⊤kˉs. So the two sums are ∑t≥uqt⊤qˉt and ∑s≥uks⊤kˉs, and their difference is the formula.
The second formula is the chain rule, ∂L/∂ℓu=auaˉu. □
Interview
Backpropagate through gated linear attention
Implement linear_attention_backward(Q, K, V, log_a, dO). Q and K have shape (T, d_k), V and dO have shape (T, d_v), and log_a has shape (T,). The forward pass is S0=0, St=atSt−1+ktvt⊤, and ot=St⊤qt, with at=eℓt and ℓt = log_a[t]. Row t of dO is oˉt=∂L/∂ot.
Return (dQ, dK, dV, dlog_a), the gradients with the shapes of the inputs, where dlog_a[t] is ∂L/∂ℓt.
Hold at most one dk×dv state at a time, never an array of per-token states.
Never divide by a decay. Decays may be exactly zero (log_a = -inf, a reset).
import numpy as np
def linear_attention_backward(Q, K, V, log_a, dO):
Q, K, V, dO = (np.asarray(x, dtype=float) for x in (Q, K, V, dO))
dV = np.stack([np.outer(Q[t], dO[t]).T @ K[t] for t in range(len(Q))])
return np.zeros_like(Q), np.zeros_like(K), dV, np.zeros(len(Q)) # placeholder: dV sees only the read at its own step
Tests, verified solutions, and AI help, open once you add your API key.
Long products without underflow
Every algorithm in this chapter needs segment products at:s=at⋯as+1. They fill the mixing matrix of §21.1, and below they fill every chunk. There are two tempting formulas. The first divides cumulative products, at:s=Pt/Ps with Pt=a1⋯at from np.cumprod. The second is its log-space twin, exp(St−Ss) with the cumulative log St=∑r≤tlogar. §11.4 warned against the ratio, and §21.1 against both. Here is what actually happens in float32.
Python · cell 4
The quotient is 1.0, a confident claim that a hundred decays of 0.9 left the state untouched. Here is why. The smallest normal float32 is 1.18×10−38. Below it lie the subnormal numbers, which keep fewer and fewer significant bits as they shrink. The product passes into them at step 829. From step 964 on, multiplying by 0.9 rounds back to the same value, 5.6×10−45, so P1100=P1000 exactly.
Hardware makes it worse. Many GPU settings flush subnormals to zero for speed, and then the same quotient is 0/0, a NaN. A stronger decay of e−0.5 per step makes the product stick after only 206 steps. And a reset, at=0, makes every later Pt zero at once.
Log space removes the underflow but not the cancellation, the loss of accuracy when two nearly equal large numbers are subtracted. The spacing of float32 at a number is the gap to its nearest neighbor, and it grows with the number's size. After 100,000 steps the cumulative log is about −10,600. Neighboring float32 numbers there are 2−10≈0.001 apart, so each of the 20 additions between Ss and St rounds to a multiple of about 0.001. The difference St−Ss is off by a few thousandths.
Since ex+ϵ≈ex(1+ϵ), the segment product is off by the same amount in relative terms, 3×10−3 in this run. Summing only the segment's own 20 logs is accurate to float32's precision, 10−7 or better. This matters most for decays near 1, which are where models keep their long memories, and §21.1 showed how sensitive those are.
There are three safe ways to get segment products, and all three appear in real kernels.
Let the scan carry them. The scan's first component is the product of the decays it has composed, built by multiplications only. It may underflow to zero, but then the true value is also below 10−38, so nothing is lost. No division ever happens.
Sum logs within a short window. Inside a chunk of Tc tokens, a cumulative sum of logs never exceeds Tcmaxt∣logat∣ in magnitude, so its spacing stays small. A difference of two such sums is accurate as long as no decay in the chunk is exactly 0. With Tc=64 and decays above 0.5, the sums stay above −45, where the float32 spacing is 4×10−6. This is how the chunked kernels of flash-linear-attention, the open-source library of linear-attention kernels that grew out of the GLA paper (fla-org/flash-linear-attention), compute their decays.
Build each segment from its own terms with additions only. Put logat in row t below the diagonal of a Tc×Tc matrix, with zeros elsewhere, and take cumulative sums down each column. Entry (t,s) is then ∑r=s+1tlogar, summed from its own terms with no subtraction anywhere. A reset (logat=−∞) gives −∞, hence a product of 0, instead of the NaN of −∞−(−∞). This is the masked cumulative product of Exercise 21.1b, done with sums of logs, and it is the segsum function of Mamba-2 (Dao & Gu 2024). Tri Dao's blog post on the algorithm reports that without it the basic algorithm produced NaNs immediately in training, even in float32.
An interviewer who asks how you would compute the decay mask of a gated layer is checking for exactly this. The right answer never divides, never subtracts long cumulative sums, and keeps the decays and their logs in float32.
The chunkwise algorithm
§21.1 gave three ways to compute a linear recurrence. None of them is good enough for training a layer with a matrix state.
The recurrent mode runs the loop. Its depth is T, and its work is elementwise, so it never touches the tensor cores.
The matrix mode builds the T×T mixing matrix and multiplies by it. It is all matrix products, but it costs O(T2).
The scan mode has depth logT, but it stores one dk×dv state per token, 137 GB in the example above.
The chunkwise algorithm takes the best of the first two. Split the sequence into short chunks. Inside each chunk, use the matrix mode, a small masked attention computed with matrix products. Between chunks, use the recurrent mode, carrying one state from each chunk to the next. Only that short chain of chunk states is sequential. RetNet (Sun et al. 2023), gated linear attention and its kernel library flash-linear-attention (Yang et al. 2023), Lightning Attention-2 (Qin et al. 2024), and Mamba-2 (Dao & Gu 2024) all train this way.
We derive it for a matrix state with one scalar decay per step,
St=atSt−1+ktvt⊤,ot=St⊤qt,t=1,…,T.
Here the queries and keys are qt,kt∈Rdk, the values are vt∈Rdv, the decays are at∈[0,1], and S0 is the initial state. The state St∈Rdk×dv has rows indexed by keys and columns by values, the convention of §21.2. Stack the tokens as rows, Q,K∈RT×dk and V,O∈RT×dv.
This one recurrence is several layers at once.
With at=1 it is the unnormalized causal linear attention of §21.2.
With a fixed decay for each head it is RetNet's retention.
With q→Ct, k→Bt, v→Δtxt, and at=eΔtA it is one head of Mamba-2, as §21.6 will show. Here Bt and Ct are the input-dependent write and read vectors of §21.4, Δt is the step size, A is the head's negative scalar rate, and xt is the head's input.
The GLA paper writes tokens as row vectors, St=Gt⊙St−1+kt⊤vt with ot=qtSt, where Gt holds its decays. That is the same layout as ours, so its formulas can be read directly.
Before the general formulas, here is the whole algorithm on four tokens.
Every number in that example came from one of three decays, all segment products. The mask holds the decays between tokens of the same chunk. The decay from the chunk start says how much of the incoming state each token sees. The decay to the chunk end says how much of each write survives into the outgoing state. The definition below names them.
Split the sequence into nc=⌈T/Tc⌉ chunks of length Tc, the last possibly shorter. Chunk j covers the steps t0+1,…,t0+Tc with t0=(j−1)Tc. Its rows of Q, K, V, O are Q[j], K[j], V[j], O[j]. The state after it is S[j]=St0+Tc, with S[0]=S0. Inside a chunk we index by the local position r=1,…,Tc.
Read the three decays carefully, because each one hides a classic off-by-one error.
The mask's diagonal is at:t=1. A token's own write is not decayed before it is read, because this chapter updates the state first and then reads it.
The decay from the chunk start includes the first token's decay, (λ)1=at0+1. Step t0+1 decays the state it inherits before adding its write.
The decay to the chunk end excludes the writing token's own decay, so (γ)Tc=1.
In plain words, each output is what its own chunk wrote, read through a small masked attention matrix, plus what all earlier chunks wrote, read from the one state that summarizes them. The state is updated once per chunk by a single matrix product, and only that update is sequential.
∂ Proof that the chunkwise form equals the recurrence
By induction on j. The claim for j−1 gives S[j−1]=St0, and for j=1 this is S[0]=S0.
Unroll the recurrence from step t0, exactly as §21.1 unrolled the scalar one, with the matrix write ksvs⊤ in place of the scalar write. For t=t0+r,
St=at:t0St0+s=t0+1∑tat:sksvs⊤.
The induction step is at+1(at:t0St0+∑sat:sksvs⊤)+kt+1vt+1⊤, using at+1at:s=at+1:s. Then
The second term is row r of (Q[j]K[j]⊤⊙L[j])V[j], since the masked product has entries (qt⊤ks)at:s for s≤t and zeros after. The first term is row r of diag(λ[j])Q[j]St0.
Setting t=t0+Tc in the unrolled state gives St0+Tc=α[j]St0+∑s(γ[j])s−t0ksvs⊤, and ∑sγsksvs⊤=K[j]⊤diag(γ[j])V[j]. That is the claim for j. A shorter last chunk changes nothing in the argument. □
Two limits make the trade-off visible. With Tc=1 every chunk is one token, the mask is the scalar 1, and the algorithm is the recurrence. With Tc=T there is one chunk, the inter-chunk term vanishes when S0=0, and the algorithm is the quadratic matrix mode. In between, the chunk length decides how much work is done as dense matrix products and how many sequential steps remain.
The picture behind this is the sequence-mixing matrix of §21.1, the T×T lower-triangular matrix M with O=MV. Here its entries are Mts=at:sqt⊤ks for s≤t. The algorithm cuts M into Tc×Tc blocks. The blocks on the diagonal are computed densely. Every block below them has rank at most dk, and it is never formed, only applied through the states.
The widget draws this decomposition on a mixing matrix with T=64 and the same decay a=0.93 at every step, shaded by the decay part at−s of each entry. Each shaded triangle on the diagonal is one masked matrix product, and everything below the band is reached only through states. Click an entry to see how its decay factors into the three pieces of the definition. Press Play to run the sequential pass one chunk at a time.
How does a chunked kernel split a recurrence into matrix products and a short sequential pass?
Each triangle is one chunk's matrix product. Entries below are reached only through chunk states. Click an entry.
Loading interactive figure…
Interview
Chunkwise gated linear attention
Implement chunked_gla(Q, K, V, log_a, T_c, S0=None) for St=atSt−1+ktvt⊤ and ot=St⊤qt.
The inputs are Q and K of shape (T, d_k), V of shape (T, d_v), log_a of shape (T,) holding logat≤0, the chunk length T_c, and the initial state S0 of shape (d_k, d_v), zeros when None.
The chunk length need not divide T and may exceed it.
Return (O, S), the outputs of shape (T, d_v) and the final state.
Use the chunkwise form, with a Python loop over chunks only and matrix products inside each chunk.
The result must be exact to float64 rounding, and finite when decays are strong, sequences are long, and some at=0 (log_a = -inf, a reset).
import numpy as np
def chunked_gla(Q, K, V, log_a, T_c, S0=None):
# placeholder: plain causal linear attention, every decay treated as 1
T = Q.shape[0]
O = ((Q @ K.T) * np.tril(np.ones((T, T)))) @ V
return O, K.T @ V
Tests, verified solutions, and AI help, open once you add your API key.
How big should a chunk be
Longer chunks put more of the work into matrix products and leave fewer sequential steps. But the work inside a chunk grows with the square of its length. So there is a balance, and it can be computed. Count the matrix-product FLOPs of one chunk, where a product of an m×k and a k×n matrix costs 2mkn.
The intra-chunk scores Q[j]K[j]⊤ cost 2Tc2dk, and their product with V[j] costs 2Tc2dv.
The inter-chunk read Q[j]S[j−1] costs 2Tcdkdv, and the state update K[j]⊤diag(γ)V[j] costs another 2Tcdkdv.
In plain words, lengthening the chunks moves work from the cheap linear part into the quadratic part and removes sequential steps. Below Tc∗ the quadratic part is the smaller one, so the total stays under twice the unavoidable linear part.
The balance point is the same as the crossover of §21.2, the length at which the two orders of multiplying out linear attention cost the same, 2dϕdv/(dϕ+dv), with dϕ the width of the queries and keys (our dk). That is no coincidence. A chunk is a short sequence, and for short sequences the quadratic order is the cheaper one.
Three hardware facts pin the choice down.
Tile size. Tensor cores multiply fixed small tiles, 16×8×16 for the bf16 instruction of §11.5. Chunks shorter than 16 leave the tiles mostly empty or put the products back on the slow units. The GLA paper sets its chunk length to a multiple of 16 for this reason.
Shared memory. A chunk's Q, K, V, its Tc×Tc scores, and the float32 state must fit in the fast on-chip memory of one streaming multiprocessor (SM), one of the GPU's 132 independent processors, about 228 KB on an H100. With Tc=64 and d=128, that is 48 KB of bf16 inputs, 16 KB of float32 scores, and 64 KB of state, which fits. At Tc=256 the scores alone would take 256 KB.
Latency. Each of the nc sequential steps costs a fixed delay, so very short chunks lose to synchronization even when their FLOPs are low.
In practice, flash-linear-attention's chunked kernels default to Tc=64. Mamba-2's default is 256, with a state of 128×64 per head, above its balance point of 85. It trades extra matrix-product FLOPs for fewer sequential steps.
The cell sweeps Tc on a small head in NumPy. Every chunk length gives the recurrence's outputs to rounding, and the FLOPs follow the theorem.
Python · cell 5
At Tc=1024 the algorithm does 29 times the FLOPs of Tc=4. In NumPy the fastest chunk is usually 16 or 64. There, Python's overhead per chunk, paid once per sequential step, balances the quadratic FLOPs, near the theorem's Tc∗=32. On a GPU the overhead per step is smaller and the matrix products far faster, but the curve has the same shape.
Research
How big should a chunk be?
Write the cost model of this section as code, in four functions.
chunk_cost(T, T_c, d_k, d_v) returns (matmul_flops, sequential_steps, state_numbers) for one head. Count every chunk as full length, since the last chunk is padded. With nc=⌈T/Tc⌉, the FLOPs are nc(2Tc2(dk+dv)+4Tcdkdv), the sequential steps are nc, and the stored states are ncdkdv numbers. Do not skip the masked half. Return Python integers, since the counts exceed 231.
step_time(T, T_c, d_k, d_v, heads, peak_matmul, peak_scalar, latency) returns the model time in seconds. It is heads times the FLOPs, divided by peak_matmul when Tc≥16 and by peak_scalar otherwise, plus nc times latency. The heads run in parallel, so the latency is paid once per step.
best_chunk(T, d_k, d_v, heads, peak_matmul, peak_scalar, latency, candidates) returns the candidate with the smallest time, and the smaller candidate on a tie.
balanced_chunk(d_k, d_v) returns Tc∗=2dkdv/(dk+dv) as a float.
Tests, verified solutions, and AI help, open once you add your API key.
At real scale
Now one real layer. Qwen3.5-397B-A17B, released in February 2026, has 60 layers, and 45 of them are Gated DeltaNet layers. Each has 64 value heads whose states are 128×128 (model card).
Gated DeltaNet is a linear-attention layer with a scalar decay per head and a write that corrects what the state stores under a key instead of only adding to it, the delta rule of §21.6. The delta rule needs an extra step inside each chunk to build the WY factors of that section, which adds work rather than replacing any. So here we count only the gated-linear-attention part of the work. We also treat each value head as its own head. The real layer shares queries and keys across groups of four value heads, which saves part of the intra-chunk work.
Python · cell 6
Dao and Gu (2024) measured the outcome of this arithmetic. Their chunked kernel for Mamba-2 is 2 to 8 times faster than Mamba's fused scan at state size 64. It allows states 8 times larger with little slowdown. And it overtakes FlashAttention-2 from 2K tokens on, reaching 6 times faster at 16K.
When a scan is the wrong tool
A parallel scan buys depth. Depth matters only when there is not enough other parallel work to fill the machine, and on a GPU there usually is. Brent's theorem counts all the work, including the independent channels and the batch.
Take one Mamba layer at the size of Mamba-2.8B, with din=5120 channels of N=16 state entries, on a batch of 8 sequences. That is 8⋅5120⋅16=655,360 independent scalar recurrences. An H100 has 132⋅128=16,896 float32 lanes, the arithmetic units that each run one thread's instruction. Give each lane a few dozen recurrences to run one after another, and the GPU is already full, with depth T and no extra work at all. A scan over time pays off when the batch is small, or inside one thread block, where a few hundred threads share one chunk of the sequence.
So what limits a recurrent layer is memory traffic, not depth, and the winning kernels are designed around bytes. Three designs show it.
Mamba keeps the scan in fast memory. Mamba's kernel loads Δ, A, B, C, and x for a block of the sequence into the on-chip shared memory (SRAM). It discretizes there, runs a work-efficient parallel scan there, and writes only the outputs, so the state tensor of shape (batch, T, din, N) never reaches HBM. The backward pass recomputes the states instead of storing them. The authors report the fused scan 20 to 40 times faster than a standard scan in PyTorch (Sections 3.3 and 4.5 of Gu & Dao 2023).
Griffin keeps a sequential loop in fast memory.De et al. (2024) note that the associative scan does nothing about memory traffic, their real bottleneck, and they measured it significantly slower on TPU-v3 than JAX's native sequential scan. They wrote a sequential scan in Pallas, JAX's language for custom kernels, that keeps the state in on-chip memory throughout. It runs almost 3 times faster than JAX's native sequential scan.
Mamba-2 and linear attention use chunks. When the state is a matrix and the transition is a scalar, or a diagonal over the keys, the chunkwise algorithm turns most of the work into tensor-core matrix products and writes one state per chunk. Mamba-1's decay for channel d and state entry n is eΔt,dAd,n, with a step size Δt,d per channel and a learned rate Ad,n per channel and state entry. As a matrix over channels and state entries it is full rank, and such a decay has no matrix-product form (Yang et al. 2023 discuss this). Making the decay one scalar per head is exactly the restriction Mamba-2 accepted to get one (Dao & Gu 2024).
The widget races these strategies under a simple roofline model. Each strategy's time is the larger of its compute time and its memory time, plus a fixed latency for every dependent step, 0.1 µs on chip and 5 µs for each round of a scan. The matrix state is the Gated DeltaNet layer of the example above, and the diagonal state one Mamba-2.8B layer, both at 32,768 tokens on an H100. The scans write every state to HBM. Switch between a matrix state and a diagonal one, and watch the winner change. Then raise the number of sequences in the batch, and see that the winner stays the same on both.
Which way of running a linear recurrence is fastest on a real GPU, and why?
Model time for one layer at 32,768 tokens on an H100. Each bar says what limits it.
Loading interactive figure…
strategy
work
depth
HBM traffic
tensor cores
used by
sequential loop, state in HBM
O(TN)
T
every state, read and written
no
naive framework code
sequential, state on chip
O(TN)
T
inputs and outputs only
no
Griffin's recurrent kernel, decoding in every model
Hillis-Steele scan
O(TNlogT)
log2T
every element, each round
no
within a warp
Blelloch scan
O(TN)
2log2T
every element, several times
no
S5 (JAX's associative scan)
work-efficient scan, state on chip
O(TN)
logarithmic inside each block of the sequence, blocks in order
Here N is the number of state entries per recurrence (dkdv for a matrix state), and a warp is the group of 32 GPU threads that run in lockstep. S5 is a diagonal state-space layer computed with JAX's associative scan (Smith et al. 2022).
Splitting one sequence across devices
The chunkwise form has one more consequence, and it matters for training on a million tokens. Context parallelism splits one sequence across several GPUs, so that no device holds the whole thing. For a recurrent layer, make each chunk a device.
Device p of P holds a contiguous slice of the sequence. It runs the chunkwise algorithm on its slice from a zero initial state. That gives its local outputs, its local final state Sploc, and its total decay αp. The pair (αp,Sploc) is the composed affine map of the device's whole slice. The true state entering device p is the exclusive scan of these pairs under ∙, a scan over only P elements. It is computed by passing one state from device to device, or in log2P rounds.
Each device then adds the correction diag(λ)QSin to its outputs, the inter-chunk term of the definition. The state-passing test of Exercise 21.5d runs the sequential version of this on two pieces.
Dao and Gu (2024) describe exactly this context parallelism for Mamba-2, with communication linear in the number of devices. The catch is the sequential dependence. Device p cannot finish until the maps of devices 1 to p−1 have been composed. But the local work comes first and is fully parallel, and only the cheap correction waits. In a hybrid model the attention layers still need ring attention or another context-parallel scheme, and they dominate the communication.
In PyTorch
Core PyTorch has no linear-recurrence scan in its stable API. Production layers call fused Triton or CUDA kernels, from flash-linear-attention or the mamba_ssm package (state-spaces/mamba), that implement the chunked algorithm with these exact steps. Serving stacks call the same kernels (§20.7). The block below writes those steps with plain tensor operations. It builds the segment sums with Mamba-2's addition-only construction, runs a batched, multi-head chunked layer with einsums, and checks it against a loop. Then it checks the reverse-recurrence gradients of this section against autograd.
🔥 In PyTorchfor a machine with PyTorch
The decay to the chunk end reuses segsum. Appending a zero log to each chunk and reading the last row gives ∑u=s+1Tclogau for every s, again with additions only.
In JAX the scan itself is a library call, jax.lax.associative_scan(fn, elems). It takes any associative fn(earlier, later) over a tuple of arrays. It also runs in reverse (reverse=True), which is the backward pass of this section. S5 is built on it.
Read more
Blelloch (1990). Prefix Sums and Their Applications. The work-efficient scan, with a classic treatment of first-order linear recurrences as scans over pairs.
Yang et al. (2023). Gated Linear Attention Transformers with Hardware-Efficient Training. The chunkwise form with data-dependent decays, why a parallel scan must materialize matrix states, the closed-form decay gradient, and the FlashLinearAttention kernels with and without materialized chunk states.
Smith, Warrington & Linderman (2022). Simplified State Space Layers for Sequence Modeling. S5, a multi-input multi-output diagonal SSM computed with a parallel scan instead of a convolution, the first deep SSM built on the scan.
Watch next
🎬 Lecture 20: Scan Algorithm · GPU MODEThe Hillis-Steele and work-efficient scans on a real GPU, the hardware view of the scans this section extends to recurrence states.🎬 Linear Attention and Beyond (Interactive Tutorial with Songlin Yang) · Sasha RushThe author of flash-linear-attention on linear attention as a recurrence, the chunkwise form, and how its kernels split work between matmuls and state passing.🎬 Lecture 60: Optimizing Linear Attention · GPU MODESonglin Yang on tensor-core tiling for chunked linear attention, materializing chunk states or not, and why a parallel scan is the wrong tool for matrix states.