Chapter 04The same rule, tiled for the GPU

Making it parallel: the chunkwise form

This is the hard chapter. A recurrence whose write depends on its own read cannot be folded up with a prefix sum, and running 100,000 sequential steps on a GPU is not a training recipe. The way out reveals something better than a speed-up: attention and recurrence turn out to be the same computation at two settings of one knob.

The constraint

Chapter 2's state was a running sum, so a whole sequence could be accumulated in parallel. Chapter 3's state is not: \(u_t\) needs \(S_{t-1}\), which needs \(u_{t-1}\). The naive prefill loop below is correct, trains nothing at a useful speed, and additionally needs \(O(d^2)\) of live memory at every one of the \(T\) steps.

S = torch.zeros(b, h, dh, dh) if cache is None else cache
outs = []
for i in range(t):                      # t = 131,072 for a long-context batch
    v_old = k[:,:,i:i+1] @ S
    u_i   = beta[:,:,i:i+1] * (v[:,:,i:i+1] - v_old)
    S     = S + k[:,:,i:i+1].transpose(-1,-2) @ u_i
    outs.append(q[:,:,i:i+1] @ S)
o = torch.cat(outs, dim=2)

Warm-up: chunking plain linear attention

Start with the easier case, chapter 2's purely additive state, and cut the sequence into blocks of \(C\) tokens. Write \(Q_r, K_r, V_r \in \mathbb{R}^{C\times d}\) for the rows belonging to block \(r\), and \(S_{r-1}\) for the state as it stood when the block began. Then for every token in the block at once:

\[ O_r \;=\; \underbrace{Q_r\,S_{r-1}}_{\textbf{inter}:\ \text{all history before this block}} \;+\; \underbrace{\mathrm{tril}\big(Q_r K_r^{\top}\big)\,V_r}_{\textbf{intra}:\ \text{real masked attention, inside}} \qquad\qquad S_r \;=\; S_{r-1} + K_r^{\top}V_r \] 4.1

Look at the two terms. They are the same triple product bracketed the two different ways from chapter 2, chosen independently for two different ranges:

THE CAUSAL MASK, CUT INTO CHUNKS OF C = 3 The dark blocks on the diagonal are computed exactly, as masked C×C attention. o_intra = tril(q_c @ k_cᵀ) @ v_c Everything below the diagonal — which is most of the matrix — is never computed. It has already been folded into S, and comes back in one matmul. o_inter = q_c @ S grows as C² per block, so total 2LCd flat at 2Ld², whatever C is
Two orders of the same product, one picture. The dark diagonal blocks are real attention with a real mask — score first. Everything below them is handled by the recurrent state — state first. Full attention is the case where the whole lower triangle is one dark block; a pure RNN is the case where each block is a single cell. Chunking is the family in between.

C is a hardware parameter, not a mathematical one

Both terms compute the same numbers regardless of \(C\). But their costs move in opposite directions. Per token, the work splits cleanly:

\[ \underbrace{2Ld^2}_{\text{state work — does not depend on }C} \;+\; \underbrace{2LCd}_{\text{the diagonal score blocks — linear in }C} \]

Set \(C = L\) and the second term becomes \(2L^2d\): full quadratic attention, recovered exactly. Set \(C = 1\) and it vanishes: the pure recurrence of chapter 2, with the fewest FLOPs of any setting. Fewer FLOPs, and yet nobody runs \(C=1\).

The reason is that a GPU's matrix units do not consume FLOPs, they consume tiles. Tensor-core instructions operate on fixed shapes — 16×8×16 on Ampere and Hopper, larger still with Blackwell's UMMA — and a matrix–vector product (\(C=1\)) fills almost none of that shape while reading the same operands. Its arithmetic intensity is about 1 FLOP per byte, the same pathology as chapter 1. At \(C = 64\) the intra-block matmul has intensity around \(C/2\), and the units are actually busy.

CHUNK SIZE C, FROM PURE RECURRENCE TO FULL ATTENTION  (d = 128) FLOPs always rises with C wall-clock time (schematic) C = 64 … 128 18·512L chunk size C (log scale) → C = 1 is a pure RNN. C = L is full softmax-shaped attention. Same answer at both ends.
The dial. Arithmetic is minimised at C = 1 and maximised at C = L, monotonically. Wall-clock time is not: it is dominated at small C by the matrix units sitting idle and at large C by the quadratic term. With d = 128 and C = 64 the extra intra-block work adds about 50% to the FLOP count of a pure recurrence — and still beats full attention at 8k context by roughly 40×.

Now the delta twist

Equation 4.1 hoisted \(S_{r-1}\) out of the block because \(V_r\) did not depend on it. Under the delta rule it does. The two offending lines:

v_old = k_i @ S           # S here is the state *at position i*, not at the block start
u_i   = b_i * (v_i - v_old)

Every correction needs the state as it stood at its own position, and that state includes the corrections of every token before it inside the same block. There is no version of this you can hoist out.

The resolution: it is a triangular system

Unroll the recurrence within one block, holding \(S = S_{r-1}\) fixed at the block's entry. By definition \(S_{i-1} = S + \sum_{j

\[ u_i \;=\; \beta_i\Big(v_i \;-\; k_i S \;-\; \sum_{j4.2

Read that carefully. Each \(u_i\) depends on the earlier \(u_j\) — but linearly, with known coefficients \(k_i \cdot k_j\). A set of linear equations in which each unknown depends only on earlier unknowns is a triangular system, and triangular systems are solved in closed form:

\[ \underbrace{\begin{bmatrix} 1 & & & \\ \beta_2 a_{21} & 1 & & \\ \beta_3 a_{31} & \beta_3 a_{32} & 1 & \\ \vdots & \vdots & \ddots & \ddots \end{bmatrix}}_{\textstyle I + \mathrm{diag}(\beta)\,A} \begin{bmatrix} u_1 \\ u_2 \\ u_3 \\ \vdots \end{bmatrix} = \begin{bmatrix} \beta_1\big(v_1 - k_1 S\big) \\ \beta_2\big(v_2 - k_2 S\big) \\ \beta_3\big(v_3 - k_3 S\big) \\ \vdots \end{bmatrix} \]

A = tril(K Kᵀ, −1), the strictly lower-triangular matrix of key–key dot products inside the block. Everything on the left is known before any \(u\) is computed.

Invert once and both dependencies fall out separately. Writing \(T = \big(I + \mathrm{diag}(\beta)A\big)^{-1}\), \(K_\beta = \mathrm{diag}(\beta)K\), \(V_\beta = \mathrm{diag}(\beta)V\):

\[ U \;=\; \underbrace{T\,V_\beta}_{\textstyle U_\beta,\ \text{does not involve }S} \;-\; \underbrace{T\,K_\beta}_{\textstyle W}\;S \qquad\Longrightarrow\qquad U \;=\; U_\beta - W\,S_{r-1} \] 4.3

This is the WY representation, borrowed from the numerical-linear-algebra literature on products of Householder matrices — which is exactly what \(\prod_j (I - \beta_j k_j^{\top}k_j)\) is. \(U_\beta\) and \(W\) depend only on \(K, V, \beta\), so they are computed for every block in the sequence in parallel, before any state exists. The only thing left that touches \(S\) is one matrix product per block.

The punchline

Once you have \(U\), the delta rule is ordinary linear attention with \(u\) substituted for \(v\). Equation 4.1 applies verbatim:

\[ O_r = Q_r S_{r-1} + \mathrm{tril}\big(Q_rK_r^{\top}\big)U_r, \qquad S_r = S_{r-1} + K_r^{\top}U_r \]

All the difficulty of chapter 3 is confined to computing a corrected value vector. Everything downstream is unchanged.

The implementation

def chunk_delta_rule_forward(Q, K, V, beta, C):
    L, d = Q.shape
    Q, K, V = map(lambda x: x.reshape(-1, C, d), [Q, K, V])   # → (L/C, C, d)
    beta    = beta.reshape(-1, C)
    K_beta  = K * beta.unsqueeze(-1)
    V_beta  = V * beta.unsqueeze(-1)

    # ---- invert (I + diag(β)A) by forward substitution. C×C, all blocks at once ----
    T = -(K_beta @ K.t()).tril(-1)
    for i in range(1, C):
        T[i, :i] = T[i, :i] + (T[i, :, None] * T[:, :i]).sum(-2)
    T += torch.eye(C)
    W = T @ K_beta        # the part of U that depends on the incoming state
    U = T @ V_beta        # the part that does not

    # ---- now: exactly the chunked linear-attention loop, with U in place of V ----
    S = torch.zeros(d, d)
    O = torch.empty_like(V)
    for i in range(L // C):
        q_i, k_i, w_i = Q[i], K[i], W[i]
        u_i     = U[i] - w_i @ S           # every correction in the chunk, at once
        o_inter = q_i @ S                  # history before this chunk
        A_i     = (q_i @ k_i.t()).tril()   # masked attention inside the chunk
        o_intra = A_i @ u_i
        S      += k_i.t() @ u_i
        O[i]    = o_intra + o_inter
    return O.reshape(L, d)

Three things about this code are worth naming explicitly.

The for i in range(1, C) loop is sequential — and that is fine
It is forward substitution, and it terminates because a strictly lower-triangular \(C\times C\) matrix is nilpotent: the Neumann series \((I+L)^{-1} = I - L + L^2 - \cdots\) has at most \(C\) terms. The point is that the sequential depth is now \(C\), not \(L\). Sixty-four steps on a tiny matrix, not 131,072 steps on the full state. That is the entire trick.
The second loop is sequential too, over \(L/C\) blocks
Unavoidable — the state genuinely is a sequential object. But there are \(L/C\) iterations instead of \(L\), each doing dense matmuls that saturate the hardware, and the expensive parallel work (\(T\), \(W\), \(U\)) already happened for all blocks simultaneously.
Intermediate states are never materialised
The naive loop needs \(S_i\) at every position — \(O(d^2)\) live memory per token, which for backprop is ruinous. Equation 4.3 produces all the \(u_i\) using only \(O(d)\) per token, because the state dependence was factored into the single term \(W S_{r-1}\). The paper's memory-efficiency claim is this, not a kernel detail.

What the block actually looks like now

MHA TRANSFORMER · GPT-2 LayerNorm Multi-Head Self-Attention + residual LayerNorm MLP · GELU + residual × 12 Q proj K proj V proj softmax(QKᵀ/√d)·V O proj state: 2Nd, grows DELTANET TRANSFORMER RMSNorm DeltaNet + residual RMSNorm SwiGLU + residual × N Q proj K proj V proj β proj convconvconv L2 normL2 norm delta rule · chunkwise RMSNorm O proj state: d², constant
The first comparison point. The outer shape is unchanged from GPT-2 — norm, mix across tokens, residual, norm, mix across channels, residual. Inside the attention slot, softmax has been replaced by a state machine with three additions: a β projection that predicts the write strength, short causal convolutions on q/k/v (cheap local mixing that recurrent models lean on heavily), and L2 normalisation to make the unit-norm assumption of chapter 3 true rather than hoped for.
Where the pressure moves next

DeltaNet can now be trained at scale, and it can precisely replace any fact it is handed a replacement for. It still has no way to say "that entire document is finished, release the capacity." Erasure remains a side effect of writing, never an operation in its own right. Chapter 5 adds the missing verb — and shows that the natural way to add it, a scalar decay, has its own ceiling.