CHAPTER 11
How can attention store less?
How can attention combine the same values without storing two full intermediate matrices?
Start with the operation
PyTorch
import torch
n, d = 8, 4
q = torch.tensor([[(i + j) % 5 - 2 for j in range(d)]
for i in range(n)], dtype=torch.float32, device="cuda") * 1
k = torch.tensor([[((2 * i + j) % 7 - 3) / 2 for j in range(d)]
for i in range(n)], dtype=torch.float32, device="cuda")
v = torch.tensor([[(i + j) % 9 - 4 for j in range(d)]
for i in range(n)], dtype=torch.float32, device="cuda")
scores = q @ k.T / d**0.5
weights = torch.softmax(scores, dim=-1)
out = weights @ v
print(out[0])Each row of q is a query: what one token looks for. Each row of k is a key: what another token offers for comparison. The matching row of v is its value: the numbers to combine. Here each has eight rows of four numbers.
k.T transposes K, making each key row a column. q @ k.T then compares every query with every key. Dividing by √4 scales the scores. dim=-1 selects the last dimension, so softmax(..., dim=-1) turns each score row into non-negative weights summing to one; weights @ v forms a weighted sum of value rows. out[0] is the four-number result for query zero. There is no masking or dropout in this example.
You choose the tensors and operations. PyTorch selects implementations, allocates results and launches GPU work if the tensors are on a CUDA device. This complete opening puts its tensors on an NVIDIA GPU with device="cuda". The browser computes the same mathematical example in JavaScript double precision; the PyTorch code uses FP32, so small numerical differences are expected.
Run the examples below in your browser with no installation or GPU required. The browser simulates the calculation; it does not execute PyTorch or GPU code. To run the PyTorch snippet itself, use a Python environment with PyTorch and an NVIDIA GPU.
Idea 1 of 3
Follow one weighted result
Definitions
- Score
- A query–key dot product divided by the square root of their width.
- Softmax
- A conversion from scores to non-negative weights that sum to one along a chosen dimension.
Start with query zero. A high score gives a key more weight, but the output still combines its value with the others. The table shows how the score row becomes weights. Each output component adds those weights multiplied by the corresponding component of every value row; the four-component result is shown beside it.
Run, then step from scores to weights to the result. Increasing the query multiplier multiplies all scores in this example. It makes the largest-score keys more influential; tied largest scores can share the weight.
Try it
Predict whether the weights sum to one. Run and follow the three steps. Change only the query multiplier to 1,000: which keys now supply almost all the result, and why are there two?
Inspect the PyTorch example for these settings
PyTorch
import torch
n, d = 8, 4
q = torch.tensor([[(i + j) % 5 - 2 for j in range(d)]
for i in range(n)], dtype=torch.float32, device="cuda") * 1
k = torch.tensor([[((2 * i + j) % 7 - 3) / 2 for j in range(d)]
for i in range(n)], dtype=torch.float32, device="cuda")
v = torch.tensor([[(i + j) % 9 - 4 for j in range(d)]
for i in range(n)], dtype=torch.float32, device="cuda")
scores = q @ k.T / d**0.5
weights = torch.softmax(scores, dim=-1)
out = weights @ v
print(out[0])Predict the result, then press Run.
2, 1, -1.75, -1, 1.5, 0.5, -2.25, 2
0.304404, 0.111984, 0.007159, 0.015155, 0.18463, 0.067922, 0.004342, 0.304404
-0.593223, 0.406777, -1.332858, -0.371937
| Key index | Score | Weight | Value component 0 | Weight × value |
|---|---|---|---|---|
| 0 | 2 | 0.304404 | -4 | -1.217615 |
| 1 | 1 | 0.111984 | -3 | -0.335952 |
| 2 | -1.75 | 0.007159 | -2 | -0.014318 |
| 3 | -1 | 0.015155 | -1 | -0.015155 |
| 4 | 1.5 | 0.18463 | 0 | 0 |
| 5 | 0.5 | 0.067922 | 1 | 0.067922 |
| 6 | -2.25 | 0.004342 | 2 | 0.008684 |
| 7 | 2 | 0.304404 | 3 | 0.913212 |
- Sum of query-0 weights
- 1
- out[0, 0]
- -0.593223
Selected phase: Write full score matrix. Each score compares query zero with one key; the same weight combines all four components of that key’s value.
Softmax is evaluated stably by subtracting the largest score before exponentiating. This preserves the normalised weights and avoids exponentiating a large positive score. Larger scores do not make the weighted result grow by the same multiplier.
Idea 2 of 3
Count the matrices between input and output
Definition
- Materialised intermediate
- A temporary result stored in full so that a later operation can read it.
The straightforward expression stores an N × N score matrix and an N × N weight matrix. For each matrix this accounting counts one write and one later read. It assumes four-byte FP32 storage, even though browser arithmetic uses JavaScript doubles.
The two matrices therefore account for 4N² value transfers × 4 bytes = 16N² bytes. These are selected intermediate reads and writes, excluding Q, K, V, output, caching and all other traffic. They are not measured DRAM transfers.
Try it
Predict what doubling N from 8 to 16 does to the intermediate byte count. Run at 8, change only N to 16, and explain the factor using rows and columns.
Inspect the PyTorch example for these settings
PyTorch
import torch
n, d = 8, 4
q = torch.tensor([[(i + j) % 5 - 2 for j in range(d)]
for i in range(n)], dtype=torch.float32, device="cuda") * 1
k = torch.tensor([[((2 * i + j) % 7 - 3) / 2 for j in range(d)]
for i in range(n)], dtype=torch.float32, device="cuda")
v = torch.tensor([[(i + j) % 9 - 4 for j in range(d)]
for i in range(n)], dtype=torch.float32, device="cuda")
scores = q @ k.T / d**0.5
weights = torch.softmax(scores, dim=-1)
out = weights @ v
print(out[0])Predict the result, then press Run.
8 × 8 = 64 values
8 × 8 = 64 values
8 × 4 values
| Counted intermediate | Values written | Values read | Bytes at 4/value |
|---|---|---|---|
| Scores | 64 | 64 | 512 |
| Weights | 64 | 64 | 512 |
- Full-matrix values written
- 128
- Intermediate reads + writes
- 1,024 bytes
These totals describe the complete calculation. Counts exclude inputs, output and hardware cache effects; they are not a timing result.
Idea 3 of 3
Combine one key tile at a time
Definitions
- Online softmax
- A running normalisation that updates when another group of scores arrives, without keeping the complete score row.
- Normalisation total
- The sum of unnormalised weights; divide the weighted-value total by this sum to obtain the output.
- Scratch storage
- Temporary space used while calculating a result. This experiment counts the state needed for one query.
We can visit a few keys at a time, combine their contribution, then move to the next group. Keep a running weighted total and a normalisation total instead of writing full score and weight matrices. When a larger score arrives, earlier weights need a new common scale. Multiply both previous totals by the same factor before adding the new tile’s contributions. Their ratio is preserved. The result below shows these additions for output component zero.
The current output estimate uses only the keys visited so far. It need not equal the final answer until every key has been included. The final result below compares all queries against the full-matrix reference.
This experiment fixes N = 8 and width = 4. Its single-query scratch budget is tile width + 4 + 2 floats: one tile of scores, four accumulated output components and two normalisation scalars. It excludes inputs, output, tile rereads and the browser's own arrays and trace. It is not a production kernel's allocation or a total memory-traffic estimate.
Try it
Predict whether changing the key tile from 2 to 4 changes the final result or only the intermediate steps and scratch budget. Run, step to the last tile, then change only tile width and compare.
Predict the result, then press Run.
0–1
2 / 8
-3.731059, -2.731059, -1.731059, -0.731059
-0.593223, 0.406777, -1.332858, -0.371937
| Key index | Score in this tile |
|---|---|
| 0 | 2 |
| 1 | 1 |
- Key tiles per query
- 4
- Single-query algorithm scratch
- 8 floats (32 FP32 bytes)
- Full-matrix intermediate traffic
- 0 bytes
- Maximum final difference from reference
- 4.44089e-16
Update the running totals
The previous totals use the old largest score. Multiply both by 0 to put them on the current scale, then add this tile’s contributions.
| Running quantity | Previous × scale | Tile contribution | New total |
|---|---|---|---|
| Normalisation total | 0 × 0 | 1.367879 | 1.367879 |
| Weighted total, component 0 | 0 × 0 | -5.103638 | -5.103638 |
Current output component zero = -5.103638 / 1.367879 = -3.731059. The totals use JavaScript doubles; displayed numbers are rounded and the scratch byte count assumes FP32 storage.
All 8 query results are compared with the materialised reference. A tiny difference reflects finite-precision operation order; it is not a GPU error measurement.
No full score/weight matrices are written in the online algorithm's accounting. That removes the selected intermediate transfers from the previous experiment; Q, K, V and output still exist. The browser intentionally retains arrays for inspection. This is a small algorithm model, not GPU execution or a reproduction of a production FlashAttention kernel.
Optional: inspect the running normalisation
Let m be the largest score seen, l the sum of exp(score − m), and a the four-component sum of exp(score − m) × V. For each new tile, choose its combined maximum m′. Multiply the previous l and a by exp(m − m′), then add this tile's contributions expressed relative to m′. The estimate is a / l. Both numerator and denominator change scale together. This avoids storing previous scores. Finite-precision accumulation order can still change low-order digits.
The tile result shows these quantities only after the weighted-result and storage experiments have introduced their purpose.
Try a new case
Apply it
With 16 queries and keys, width 4 and tiles of 4 keys, how many key tiles does query zero visit? Predict its scratch budget and the materialised intermediate byte count. Does choosing online attention mean no data moves?
Compare your answer
Run the example on your desktop
Download this chapter’s opening PyTorch example. See the complete example index and setup appendix for CPU PyTorch, the supported GPGPU-Sim programs and optional NVIDIA GPU execution.
The browser still requires no installation. Downloads are generated from the displayed source; CPU checks and simulator checks do not establish GPU performance.
References
Use these official references to check the concepts and code on this page.
- PyTorch: torch.softmax (new tab)
Follow its functional.softmax link to check what dim selects for normalisation.
- PyTorch: torch.matmul (new tab)
Check the matrix products that form scores and combine values.
- PyTorch: scaled_dot_product_attention (new tab)
Compare the manual calculation with the API’s dropout, masking and implementation rules.
These browser exercises do not run an installed PyTorch or CUDA toolkit. For optional local execution, choose documentation matching your installed tools. Links open online in a new tab; the questions and answer comparisons below work offline.
Use the documentation
Use the references to explain each answer, then apply it to this page’s example. No installation is needed.