The Cost of Attention

Dive into Deep Learning · §10.5

The cost of attention
quadratic complexity · online softmax and FlashAttention · sliding windows · recurrent linear attention

Sequence-layer complexity

Map n tokens of dimension d to n tokens of dimension d:

layer complexity sequential ops max path length
convolution (k) \mathcal{O}(knd^2) \mathcal{O}(1) \mathcal{O}(n/k)
recurrence \mathcal{O}(nd^2) \mathcal{O}(n) \mathcal{O}(n)
self-attention \mathcal{O}(nd^2+n^2d) \mathcal{O}(1) \mathcal{O}(1)

Sequence-layer connectivity for a convolution, a recurrence, and self-attention.

Self-attention combines full parallelism and constant path length with quadratic complexity in sequence length.

Measured quadratic scaling

One layer: 4n^2d FLOPs, two n \times n score buffers = 8n^2 bytes (fp32), per head, per sequence. Dominates the layer as soon as n > 2d.

n =  2048: XLA temp    32.0 MiB, predicted 8n^2 B =    32.0 MiB
n =  4096: XLA temp   128.0 MiB, predicted 8n^2 B =   128.0 MiB
n =  8192: XLA temp   512.0 MiB, predicted 8n^2 B =   512.0 MiB
n = 16384: XLA temp  2048.0 MiB, predicted 8n^2 B =  2048.0 MiB
  • Exact from n = 4096 up (the smallest run adds a few MiB of allocator overhead); doubling n quadruples this cost.
  • At n = 131{,}072: one fp32 attention map \approx 69 GB. Nobody stores it.

Measured runtime is quadratic

def wall_clock(f, *args, reps=10):
    f(*args).block_until_ready()  # Warm up (and compile)
    start = time.time()
    for _ in range(reps):
        f(*args).block_until_ready()
    return (time.time() - start) / reps

layer = jax.jit(attention_layer)
for n in [2048, 4096, 8192, 16384]:
    Q = jax.random.normal(jax.random.key(0), (n, d_h))
    print(f'n = {n:5d}: {wall_clock(layer, Q, Q, Q)*1e3:6.2f} ms')
n =  2048:   0.28 ms
n =  4096:   0.48 ms
n =  8192:   1.48 ms
n = 16384:   4.91 ms
  • Launch-bound while the GPU is idle; from a few thousand tokens on, each doubling of n roughly quadruples the time, consistent with the 4n^2d operation count.

Online softmax: the matrix never needs to exist

Per query, carry a running max, normalizer, and output; process keys in blocks:

m' = \max(m, \max_j a_j), \quad s' = s\,e^{m-m'} + \sum_j e^{a_j - m'}, \quad \mathbf{o}' = \mathbf{o}\,e^{m-m'} + \sum_j e^{a_j-m'}\mathbf{v}_j

\mathbf{o}/s equals \mathrm{softmax}(\mathbf{a})\mathbf{V}. When the running maximum increases, the factors e^{m-m'} rescale earlier partial sums.

Online softmax carries a running maximum, normalizer, and weighted-value sum across key blocks.

Chunked attention: exact, in linear memory

Twenty lines carry the update over all queries, one key block at a time:

n, d_h = 2048, 64
Q, K, V = (jax.random.normal(k, (n, d_h))
           for k in jax.random.split(jax.random.key(0), 3))
with jax.default_matmul_precision('highest'):
    err = jnp.abs(chunked_attention(Q, K, V)
                  - causal_attention(Q, K, V)).max()
print(f'maximum deviation: {float(err):.2e}')
maximum deviation: 4.62e-07

  • Same answer, same FLOPs; the n^2 footprint is gone.

The bottleneck is memory traffic, not FLOPs

GPUs multiply hundreds of times faster than they fetch. Naive attention writes n^2 scores to slow memory and reads them back twice. FlashAttention (Dao et al., 2022): tiles in on-chip SRAM, softmax stats in registers, backward pass recomputes instead of storing.

B, H, n = 2, 8, 8192
X = jax.random.normal(jax.random.key(0), (B, n, H, d_h), dtype=jnp.float16)

def naive_heads(X):
    Xt = X.transpose(0, 2, 1, 3)
    i = jnp.arange(X.shape[1])
    scores = Xt @ Xt.swapaxes(-1, -2) / math.sqrt(X.shape[-1])
    scores = jnp.where(i[None, :] > i[:, None],
                       jnp.finfo(scores.dtype).min, scores)
    return (jax.nn.softmax(scores, axis=-1) @ Xt).transpose(0, 2, 1, 3)

def fused_heads(X):
    return jax.nn.dot_product_attention(X, X, X, is_causal=True,
                                        implementation='cudnn')

for name, f in [('naive', naive_heads), ('fused', fused_heads)]:
    jitted = jax.jit(f)
    temp = jitted.lower(X).compile().memory_analysis().temp_size_in_bytes
    print(f'{name}: {wall_clock(jitted, X)*1e3:6.2f} ms, '
          f'XLA temp {temp/2**20:7.1f} MiB')
naive:   9.71 ms, XLA temp  4096.0 MiB
fused:   1.46 ms, XLA temp     0.0 MiB
  • Exact attention with no quadratic score buffer; use scaled_dot_product_attention or jax.nn.dot_product_attention when the corresponding optimized backend is available.

Attention through a window

If each query attends to the w most recent positions, the causal mask becomes a band:

  • \mathcal{O}(nw) work instead of \mathcal{O}(n^2).
  • Deployed: Mistral 7B (w = 4096, 32 layers); Longformer adds global tokens; NSA learns the sparsity pattern itself.

Depth restores the reach

Information hops w-1 positions per layer: receptive field 1 + L(w-1) after L layers, as the following check confirms:

depth 1: last query reaches 8 positions (formula: 8)
depth 2: last query reaches 15 positions (formula: 15)
depth 4: last query reaches 29 positions (formula: 29)
  • The CNN bet (ch. 7), re-made: locality compounds through depth.

Linear attention: kernelize the score

Choose a factorizing kernel \phi(\mathbf{q})^\top\phi(\mathbf{k}) instead of \exp(\mathbf{q}^\top\mathbf{k}/\sqrt{d}). The query factors out of the attention sum:

\mathbf{o}_t = \frac{\phi(\mathbf{q}_t)^\top \mathbf{S}_t}{\phi(\mathbf{q}_t)^\top \mathbf{z}_t}, \qquad \mathbf{S}_t = \mathbf{S}_{t-1} + \phi(\mathbf{k}_t)\,\mathbf{v}_t^\top

A recurrent network: fixed d \times d matrix state, outer-product writes, queried like an associative memory (Katharopoulos et al., 2020; fast weight programmers, 1991).

Two forms, one answer

Parallel form (cumulative sums) for training; recurrent form (carry \mathbf{S}, \mathbf{z}) for generation:

n = 512
Q, K, V = (jax.random.normal(k, (n, d_h))
           for k in jax.random.split(jax.random.key(0), 3))
err = jnp.abs(linear_attention_parallel(Q, K, V)
              - linear_attention_recurrent(Q, K, V)).max()
print(f'maximum deviation: {float(err):.2e}')
print(f'recurrent state: {d_h}x{d_h} + {d_h} floats '
      f'= {(d_h * d_h + d_h) * 4 / 1024:.0f} KiB at any sequence length')
maximum deviation: 2.38e-07
recurrent state: 64x64 + 64 floats = 16 KiB at any sequence length
  • 16 KiB of state at any context length, vs. a KV cache that grows forever. The fixed-size state \mathbf{S}_t is a lossy summary of the past.

Measured time and memory

  • Dense approaches slope 2; windowed remains near the launch-overhead floor; linear attention uses nd_h^2 memory and bandwidth-bound cumulative sums. Its principal advantage is the constant-memory recurrent mode.
  • Current approaches emphasize memory-efficient exact attention, windowed attention, and linear attention in recurrent form.

Recap: three scaling strategies

  • Reorganize: online softmax → FlashAttention. Exact, linear memory; the bottleneck was traffic, not FLOPs.
  • Restrict: sliding windows; depth restores reach as 1 + L(w-1).
  • Kernelize: linear attention = linear recurrence with matrix state:

\mathbf{S}_t = \mathbf{S}_{t-1} + \phi(\mathbf{k}_t)\,\mathbf{v}_t^\top

With identity decay, this recurrence has the state-space form developed in Chapter 12. Parallel scans and learned gates lead to related models such as Mamba; attention and recurrence occupy two ends of this design space (SSD, 2024).