Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why → how

Why is attention quadratic?

Doubling the context length makes attention 4× more expensive, not 2×. That single fact shapes every trade-off in modern LLM serving — and explains what FlashAttention actually changed (it's not what most people think).

AI & ML intermediate Apr 29, 2026 · updated Aug 25, 2026 · 16 min read

On this page

The picture version

Six pictures for a reader who has never thought about what a model does with a long prompt. The prose below fills in the seams the pictures skip.

1 · The problem

Same question, same model. One of them makes you wait thirty seconds.

just the question “what’s the termination notice period?” answers immediately the same question + a 50,000-token contract “what’s the termination notice period?” a cursor, for 30 seconds The model isn’t bigger. It isn’t reading line by line. and doubling the document makes the wait roughly four times longer, not twice
Nothing about the model changed when you attached the document — not one byte of it. The pause is the cost of one operation the model performs on the prompt before it may write anything, and that cost grows faster than the prompt does.

2 · Where the wait lives

Before it writes a word, it scores every piece of your document against every other piece.

4 words 16 scores 2× 8 words 64 scores — four times as many your 50,000-token contract (a token is roughly a word) 2,500,000,000 scores in the grid and that’s one grid, for one head, in one of the model’s dozens of layers Twice the words means four times the grid. That is the whole story. a decoder-only model fills in only the lower half, which saves a factor of two, not the shape
The operation at the heart of every transformer asks every word how much it should attend to every other word. That is a grid, and a grid grows with the square of its side — so doubling the prompt quadruples the work before the first word can be written.

3 · Why you can’t just not do it

Let each word look only at its neighbours and you have changed the model.

every word may read every word one hop, straight across the property that made transformers work every word may read only its neighbours to reach the far end, the information has to be handed along a layer at a time, like a bucket brigade The cost is the price of the claim that any pair might matter. every cheaper scheme is a bet about which pairs are safe to ignore — sometimes a good bet, never a free one
The grid isn’t a sloppy implementation; it is the modelling claim itself. Drop pairs and you get a different model — cheaper, and no longer able to route information from one end of your contract to the other in a single step.

4 · Why writing feels fast and reading feels slow

The expensive part happens once, before the first word.

reading your contract the entire grid, at once nothing is stored yet, so nothing can be reused writing the answer already worked out and kept each new word adds one row against what’s kept This is why the wait is all up front, and why skipping a prompt you’ve sent before is such a big win. caching the stored rows across requests is exactly what “prompt caching” sells
Generating each word is cheap because the model keeps what it already worked out for the earlier words. Reading the prompt has no such luxury — the first pass fills the whole grid from nothing, which is the thirty seconds you sat through.

5 · The famous fix, and what it didn’t fix

FlashAttention moved the grid. It didn’t shrink it.

before the fast on-chip scratchpad the big, slow memory next door the whole grid is written out and read back, repeatedly after one tile at a time, all inside the scratchpad barely touched Same number of multiplications. Far fewer trips to slow memory. so the grid never has to exist all at once — which is what made very long prompts practical
The arithmetic was never the bottleneck; hauling the grid to and from the chip’s main memory was. Computing it in small tiles that fit in fast on-chip memory keeps the arithmetic identical and cuts the hauling — a memory fix, not a cheaper algorithm.

6 · Keep this card

There are only three ways out, and one of them changes the model.

cost of reading a prompt = its length, squared because every pair of words gets a score move it compute every score anyway, but never store the whole grid (FlashAttention) skip it don’t redo what you already did for this prefix last time (caching) redefine it stop scoring every pair — and accept a different model (windows, sparse, state-space) There is no fourth box labelled “score every pair, but cheaper.”
Picture to keep: a room where everyone must shake hands with everyone before anyone may speak — twice the people, roughly four times the handshakes. Knowing which of the three boxes a given optimisation sits in is most of understanding how long-context serving works.

Why it exists

You paste a long document — a 50,000-token contract, or your whole codebase’s worth of files — into a chat window, ask a one-line question about it, and hit enter. Then you sit there. Ten, twenty, thirty seconds of nothing, no first word, just a cursor. Ask the same one-line question with no document attached and the answer starts instantly. That 50,000-token prompt is the running example for the rest of this post.

You probably read that pause as the model “reading” your document, or as the sheer size of the model catching up with it. Neither is what’s happening. The model isn’t bigger, and it isn’t reading in any sequential sense. Memory is cheap. FLOPs are cheap. The weights of an LLM don’t grow by a single byte when you feed in a longer prompt. So why does doubling a 4k prompt to 8k make the request feel four times slower, instead of twice? Why do providers charge per token instead of per request? Why did “1M context” take so much engineering effort to ship when “100k context” already worked?

The reason has nothing to do with the model being big. It’s a property of attention itself, the operation at the heart of every transformer. Attention asks every token in the sequence to look at every other token. Every pair. A sequence of N tokens produces an N×N matrix of pairwise interactions, and the compute needed to evaluate that matrix grows as N² — quadratically. The memory footprint is N² in the naive implementation, but this turns out to be fixable, which is most of what FlashAttention is about. The compute isn’t.

That single fact is the source of nearly every cost-and-latency story in modern LLM serving. It’s why context length is rationed. It’s why prompt caching exists. It’s why long context models still feel sluggish even on H100s. The N² is not a bug or a sloppy implementation; it’s baked into what self-attention is.

Why it matters now

For most of the early transformer era, sequences were short. Translation tasks were a few hundred tokens. BERT’s default was 512. Quadratic was annoying but cheap in absolute terms.

Three things changed:

If attention were linear in N, none of this would be a problem. The entire ecosystem of tricks — sliding windows, sparse attention, KV caching, prompt caching, FlashAttention, linear-attention research — exists because it isn’t.

The short answer

attention cost ∝ N² · d (where N = sequence length, d = head dimension)

Picture to keep: a room where everyone has to shake hands with everyone before anyone may speak. Twice as many people means roughly four times as many handshakes. Where the picture breaks: handshakes are symmetric and attention isn’t — token i asking about token j is a different question than j asking about i, so it’s really every ordered pair, and a decoder-only model skips the pairs that point forward in time.

Self-attention computes a score for every pair of tokens (a query for token i times a key for token j, for all i, j). That’s an N×N matrix. Building it, softmaxing it, and using it to weight the values is unavoidably quadratic in N. You can hide where the matrix lives in memory, but you cannot avoid computing all N² entries — at least not without changing what attention means.

How it works

Let’s actually do the arithmetic on your 50,000-token prompt, because it makes the rest of the post obvious.

The thing that blows up isn’t the weights — it’s the score table. Before the model may write one word, it has to fill in how much every token of your contract should attend to every other one. Here’s where that comes from.

For one attention head with sequence length N and head dimension d_h, you have three matrices:

The math is:

scores  = Q · Kᵀ      # shape N×N
weights = softmax(scores / √d_h)   # still N×N
output  = weights · V # shape N×d_h

Look at the shapes. Q · Kᵀ produces an N×N matrix. Each cell (i, j) is the dot product between query i and key j — how much should token i attend to token j. At N = 50,000 that’s 2.5 billion cells, per head, per layer, for a prompt you’d describe as “a longish document.” There are N² cells, each costing O(d_h) to compute, so total work is O(N²·d_h). The output weights · V is another O(N²·d_h) matmul. The softmax is O(N²) — one normalization per row. Summing across H heads, total per-layer attention compute is roughly O(N²·d_model) where d_model = H·d_h is the hidden size.

Compare this to a feed-forward layer, which is O(N·d_model²) — linear in N, quadratic in the hidden size. The standard transformer expands the FFN inner dimension to ~4·d_model, so the FFN constant is bigger than attention’s by roughly that factor. Setting the two costs equal gives you a crossover, and where it lands depends on what you count. Compare only the quadratic score-and-value matmuls against a standard 4× FFN and it falls around N ≈ 4·d_model — about 16k tokens for a typical d_model ≈ 4096. Include the linear Q/K/V/output projections inside the attention block and it moves closer to N ≈ 2·d_model, about 8k. Either way the shape is the same: below the crossover the FFN is the expensive part and attention is cheap; above it, attention dominates and grows hard. Your 50k-token contract is comfortably above it on both accountings.

Why “every pair” is non-negotiable

You might ask: do we really need all N² scores? Couldn’t every token just look at, say, the last few tokens?

You can. That’s what sliding-window attention does, and what most “long context” architectures eventually fall back on for the bulk of the work. But it’s not free — you’ve changed the model. A token that only attends to a local window cannot directly route information from a distant token in one layer; the information has to hop, layer by layer, like a bucket brigade. Vanilla attention’s whole appeal was that every token could route to every other token in a single layer. Take that away and you’ve broken the property that made transformers work.

This is the actual source of the quadratic: the modeling claim that any pair of tokens might matter. The cost is the price of the claim. Approximations that drop the N² are all making some bet about which pairs you can safely ignore.

What about the softmax?

The softmax in softmax(QKᵀ / √d) is the other thing you can’t easily avoid. It’s a row-wise normalization across N entries — to compute weight (i, j), you need the sum of exp(score_{i, k}) over all k. That’s a global dependency: every column matters for every row. This is why “linear attention” research (which replaces softmax with a kernel that factors as a product) is its own subfield — without softmax, you can sometimes avoid materializing the N×N matrix at all, but you also lose some of the expressive sharpness that makes attention useful in practice. There’s no consensus on whether the trade is worth it; every open-weights frontier-class model whose architecture is published still uses softmax attention. (The closed models don’t document theirs, so that’s the boundary of what can be checked.)

KV cache makes decode linear, not prefill

A common confusion: “doesn’t the KV cache make this linear?” Sort of, but only for decode.

When you generate token N+1 autoregressively, you reuse the K and V tensors you already computed for tokens 1…N. The new token’s query attends against those cached keys, which is O(N·d) per step — linear in the cached length. Generating N tokens one at a time is therefore O(N²·d) total (the sum 1 + 2 + … + N), but each individual step is linear.

Prefill is different. When the model first reads your 50,000-token prompt, there is no cache yet. Every token’s query has to be scored against every other token’s key in one giant matmul — the full N² hit. This is why time-to-first-token grows so badly with prompt length and why prompt caching, which lets the provider skip the prefill on a reused prefix, is such a big deal.

What FlashAttention actually changed (and what it didn’t)

This is the part most people get wrong, so it’s worth being precise.

FlashAttention — the 2022 paper by Tri Dao and collaborators — did not make attention subquadratic. Its FLOP count is still O(N²·d). The paper is open about this; the contribution is “IO-awareness,” not a complexity reduction.

What it changed was where the N×N matrix lives. Standard attention writes the full N×N scores matrix out to GPU HBM, then reads it back to softmax it, then writes the result, then reads it again to multiply by V. Those reads and writes are the actual bottleneck on a GPU, because SRAM on-chip is much faster than HBM but much smaller. The original FlashAttention paper’s A100 example puts the gap at roughly 19 TB/s SRAM (20 MB total) versus 1.5 TB/s HBM (40 GB) — about 13× the bandwidth in about 1/2000th the capacity. The arithmetic units finish quickly and then sit idle waiting for memory. (Same theme as the memory bandwidth story for inference more broadly.)

FlashAttention’s trick is tiling: split Q, K, V into blocks small enough to fit in SRAM, compute the partial attention for each block fused into one kernel (scores → softmax → output, all in SRAM), and never materialize the full N×N matrix in HBM at all. Total FLOPs are the same. Total HBM traffic drops dramatically. Two different numbers get quoted from the paper and it’s worth keeping them apart: up to ~7.6× on the attention kernel itself, versus roughly 3× end-to-end on GPT-2 training and ~15% on BERT-large. The kernel number is bigger because attention is only part of the model’s total work — which is exactly the crossover point above, and exactly why the end-to-end wins grow as sequences get longer. On your 50k-token prompt, attention is most of the work, so you’re near the top of that range rather than the bottom.

(FlashAttention-2 and FlashAttention-3 are successive constant-factor wins on the same idea, FA-3 targeting Hopper-generation GPUs. Same asymptotics, better kernels.)

So: attention is still quadratic in compute. It’s also now linear in memory (you don’t store the N×N matrix), which is what made very long contexts feasible at all. The bottleneck shifted from memory I/O to actual arithmetic — which is where the N² lives, and which we don’t know how to remove without changing the model.

Where the math actually breaks down

A few subtleties worth seeing the seams on:

You started with attention cost ∝ N² · d. What did this post add? — + the N² is a modeling choice, not an implementation flaw, which is the part that tells you which optimizations can possibly exist. Every long-context trick in production is one of exactly three things: (a) hiding where the N×N matrix lives so it doesn’t blow up memory, while computing every entry anyway (FlashAttention), (b) skipping the computation entirely when you’ve done it before (prompt caching, KV cache), or (c) changing what attention means and accepting some loss of expressivity (sparse, sliding-window, linear, state-space). There is no fourth bucket labelled “compute all N² pairs, but cheaper.” Knowing which bucket a given optimization sits in is most of understanding modern serving.

And that’s the answer to the thirty-second pause you started with. The model wasn’t reading your contract; it was building a 50,000 × 50,000 grid of pairwise scores, per head, per layer, before it was allowed to emit a single token. Double the contract and that grid quadruples.

Check yourself

Before you go — you cut your prompt in half, from 50k tokens to 25k. Ignoring fixed overheads, does your prefill compute drop by 2× or by 4×?

Answer

Neither exactly — somewhere in between. Prefill cost isn’t one term. The attention part scales with N², so halving N cuts it by ~4×; the FFN part scales with N, so halving N cuts it by only 2×. Your actual saving lands between 2× and 4×, closer to 4× the more attention dominated the total, which per the arithmetic above means the longer the original prompt was. (Measured time-to-first-token will be messier still — tokenization, scheduling, and network overhead don’t shrink with N at all.) A reader who only remembers “attention is quadratic” predicts 4×; a reader who remembers attention is quadratic and everything else is linear predicts the real answer and knows which way the error goes.

And a design question: someone proposes cutting your long-context bill by switching to a model with sliding-window attention over a 4k window. What do you gain, and what specifically should you test before shipping it?

Answer

You gain linear-in-N attention: each token attends to a fixed 4k window instead of the whole prefix, so the N² term becomes N·4096. What you’ve given up is single-hop long-range routing — a token can no longer read directly from something 40,000 tokens back; that information has to hop through the window layer by layer. So test the tasks that depend on exactly that: retrieving one specific fact from deep in a long document, resolving a reference to something mentioned once at the very start, or noticing a contradiction between the beginning and end of a contract. Summarization and local-coherence tasks will likely look fine, which is what makes this trade easy to ship by accident.

Going deeper