GPU Lab / interactive book
GPU mapSource ↗

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])
Desktop setup

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])
Desktop setup

Predict the result, then press Run.

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])
Desktop setup

Predict the result, then press Run.

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.

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
Query zero visits 4 key tiles. The single-query online scratch budget is 10 floats. Materialising the two matrices accounts for 4,096 read/write bytes, versus 0 full-matrix intermediate bytes for online attention. Inputs and outputs still move; these counts exclude them and do not predict elapsed time.

References

Use these official references to check the concepts and code on this page.

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.

Install GPU Lab

Give GPU Lab its own icon and window. All lessons and simulators work offline once the course is saved.

Use your browser’s install option:

If your browser has no install option, try Chrome or Edge.

App status

Version

Checking for updates

Offline access

Not saved for offline use

Save all lessons, browser simulators, diagrams and example files. Desktop execution needs separate PyTorch or CUDA/GPGPU-Sim tools; their installations are not part of this offline download. External links need internet access.