Draft

How an attention head mixes information

In the word2vec article, we learned a table of word vectors from the contexts words appeared in during training. Words with related usage could acquire related representations. But the table has a limitation: looking up a word gives the same vector regardless of the sentence it is in. Context helped train the vector; the lookup does not consult the current context.

That is the polysemy problem we left with. In “the river bank” and “the money bank,” bank retrieves the same row. Its vector can reflect evidence from both uses, but it does not tell us which use this occurrence belongs to. The surrounding words supply that missing information.

We therefore need a step after the lookup: let each position gather information from the other positions in this sentence. An attention head computes a weighted mixture of the information they carry. The weights depend on the input, so different positions can gather different mixtures. The goal is a contribution that helps make the representation useful for this particular context.

Inside a transformer block, that contribution is projected and added to the representation already there. The head computes the mixture; the residual connection incorporates the update. This gives us a path from a shared word vector to a representation enriched by its current surroundings. The next article builds the block around these updates.

Two sentences start with the same bank vector, [0.5, 0.5, 0.8, 0.2]. A river-related attention update is added in the river bank example; a finance-related update is added in the money bank example, producing different representations.
Conceptual illustration: the base vector uses the toy numbers introduced below; the updates are symbolic, not measured results. Attention mixes values, the output projection maps the mixture into an update, and the residual connection adds it to the existing representation. The pictures illustrate information, not literal images stored in vectors. The embedding table stays unchanged during this forward pass.

A standard transformer block contains several heads in its multi-head attention sublayer. Heads within a block run in parallel; transformer blocks run sequentially. The heads read the same input representations and compute separate mixtures, which are combined and projected into one attention update. Each subsequent block receives the previous block’s updated representations, rather than looking up the initial token vectors again.

Here we will build one head on small vectors: first watch information mix, then see how learned projections control what it gathers, and finally train a tiny model to use it.

You need vectors, dot products, and the idea that multiplying by a matrix transforms a vector. The weight-matrix article develops that last step. Our axes will be hand-chosen for legibility; real embeddings do not come with a labelled finance coordinate. We also treat each word as one token throughout this toy.

One forward pass, no training

Give four tokens four numbers each. The dimensions mean finance, water, structure, and generic content in this example only:

import numpy as np

emb = {
    #           finance water structure generic
    "the":   np.array([0.0, 0.0, 0.0, 1.0]),
    "river": np.array([0.0, 1.0, 0.0, 0.2]),
    "money": np.array([1.0, 0.0, 0.0, 0.2]),
    "bank":  np.array([0.5, 0.5, 0.8, 0.2]),
}

bank starts tied between finance and water. We want to compute a contribution at its position by mixing information from the sentence. With three tokens, the shape of that computation is:

hbank=a1vthe+a2vriver+a3vbank.h_{\text{bank}} = a_1 v_{\text{the}} + a_2 v_{\text{river}} + a_3 v_{\text{bank}}.

The vv vectors carry information; the nonnegative aa weights sum to one. For now, use the embeddings themselves as those vectors. The open question is where the weights come from.

Our first rule is simple: dot the receiving token’s vector against each source vector, divide the scores by the square root of their dimension, and apply softmax. Softmax exponentiates the scores and divides each result by their sum. This makes larger scores receive larger shares while keeping the shares positive and summing to one.

Here is the reusable head. X is the sentence matrix, one token per row. We include three projection matrices now, but initially set all three to the identity, which leaves every vector unchanged.

def softmax(z):
    e = np.exp(z - z.max(axis=-1, keepdims=True))
    return e / e.sum(axis=-1, keepdims=True)

def attention(X, Wq, Wk, Wv, causal=False):
    Q, K, V = X @ Wq, X @ Wk, X @ Wv
    scores = Q @ K.T / np.sqrt(K.shape[-1])
    if causal:
        future = np.triu(np.ones(scores.shape, dtype=bool), k=1)
        scores = np.where(future, -np.inf, scores)
    A = softmax(scores)
    return A, A @ V

I = np.eye(4)
for tokens in [["the", "river", "bank"], ["the", "money", "bank"]]:
    X = np.stack([emb[t] for t in tokens])
    A, H = attention(X, I, I, I)
    print(tokens)
    print("  bank input:", X[-1])
    print("  weights:", A[-1].round(2))
    print("  mixture:", H[-1].round(2))

The result:

['the', 'river', 'bank']
  bank input: [0.5 0.5 0.8 0.2]
  weights: [0.26 0.31 0.43]
  mixture: [0.21 0.52 0.34 0.41]
['the', 'money', 'bank']
  bank input: [0.5 0.5 0.8 0.2]
  weights: [0.26 0.31 0.43]
  mixture: [0.52 0.21 0.34 0.41]

The same input vector at bank now receives different mixtures. river supplies water; money supplies finance. The mixture changes even though the weights are identical: the vectors being mixed have changed. Attention weights are only one part of the computation.

This is not yet the result of a complete transformer block. We have printed H, the head output, without an output projection or residual addition. Calling it bank’s new representation would skip those operations.

To preview the addition without introducing another matrix yet, suppose the output projection is identity and the stream has the same four coordinates. Adding the river mixture to the original gives [0.71, 1.02, 1.14, 0.61]; adding the money mixture gives [1.02, 0.71, 1.14, 0.61]. Both keep an additive contribution from the original and acquire a different context contribution. This isolates the residual operation; normalization and general output projections belong to the block article.

A plain average also separates these two sentences: the river sentence’s mean has water 0.50 and finance 0.17. So this first experiment demonstrates information mixing, not a task that requires selective attention. Our hand-designed vectors made even a simple mixture useful. Next we will see what controls that mixture and where this first matching rule falls short.

Why it mixed at all

Trace just the bank row in “the river bank.” Its unscaled dot products with the, river, and itself are [0.20, 0.54, 1.18]. Since the vectors have four dimensions, dividing by 4=2\sqrt{4}=2 gives [0.10, 0.27, 0.59]. Softmax turns those scores into approximately [0.26, 0.31, 0.43].

Then the head computes:

mixture = 0.26 × the + 0.31 × river + 0.43 × bank
        ≈ [0.21, 0.52, 0.34, 0.41]

The displayed arithmetic is rounded; the program keeps full precision. The river term contributes water to the mixture. The largest weight happens to be on bank itself: its self-dot-product is largest in this constructed space. This is a property of these numbers, not a requirement that attention preserve identity by attending to itself. In a block, the residual path separately carries the existing representation forward.

All rows do the same operation at once. With identity projections:

scores = X @ X.T / np.sqrt(X.shape[-1])  # (n, n)
A = softmax(scores)                      # (n, n): one distribution per row
H = A @ X                               # (n, d): one mixture per row

A[i, j] is the share of source position j in the mixture received at position i. It is a distribution over positions, not over next-token vocabulary choices.

Try the score scale

Multiply the scores by 5 before softmax: the largest score gets a larger share. Here that favors bank itself, not river. Multiply by 0.1 and the weights approach a uniform average. The standard sqrt(d_k) divisor controls score scale as query/key width changes; it does not choose which token should win. The section on dot products, cosine similarity, and scaling below explains why.

When raw similarity asks the wrong question

The first rule says “gather from vectors similar to mine.” But useful information need not resemble the receiving token. A structure can need an identity supplied by a name.

To see this cleanly, switch to a separate, two-dimensional teaching space. Its axes are structure and Paris identity. This is not a slice of the four-dimensional bank example:

# rows: the, Eiffel, tower
X2 = np.array([[0., 0.], [0., 1.], [1., 0.]])
I2 = np.eye(2)
A, H = attention(X2, I2, I2, I2)
print(A[-1].round(4))  # [0.2483 0.2483 0.5035]
print(H[-1].round(4))  # [0.5035 0.2483]

tower · Eiffel = 0. Under raw similarity, Eiffel scores exactly like the zero-vector filler. Some Paris content still enters because softmax gives both positions positive weight, but the matching rule has no way to prefer Eiffel over the filler.

For our chosen task, we want tower to gather identity information. That is a limitation of the matching rule, not a failure of weighted mixing. We need to change what tower matches against while keeping its original vector available.

Producing a query

Each token produces one query, one key, and one value vector per attention head, per transformer block. Within one head, the same learned matrix Wq transforms every position: q_i = x_i @ Wq, or Q = X @ Wq for the whole sentence. Separate projections Wk and Wv produce that token’s key and value. These are computed vectors, rather than additional vocabulary embedding lookups. A token in a block with 12 heads therefore has 12 queries, 12 keys, and 12 values. The query is a view used for matching; the original representation remains available to the rest of the block.

“What information could this position use from other tokens?” is a useful way to interpret a query. For bank, a hypothetical head might match water-related evidence or finance-related evidence; for tower, it might match a name that supplies identity. More precisely, the query determines which keys produce large dot-product scores. These scores become weights for mixing values. Producing a query is the matching setup; it has not yet gathered information or updated the token’s representation.

A query reads the token’s full current representation through Wq; it need not preserve raw similarity or keep the same number of coordinates. A large model can project thousands of representation coordinates into a much smaller query/key space. Queries and keys within a head must have the same width so their dot products are defined. Their coordinates do not have to be named concepts, and a head is not programmed with a literal question.

Transform the vector used to ask the question. A swap matrix maps tower = [1, 0] to a query [0, 1], which matches Eiffel’s identity coordinate:

swap = np.array([[0., 1.], [1., 0.]])
A, H = attention(X2, swap, I2, I2)
print(A[-1].round(4))  # [0.2483 0.5035 0.2483]
print(H[-1].round(4))  # [0.2483 0.5035]
print(X2 @ swap)
# [[0. 0.]    the's query
#  [1. 0.]    Eiffel's query
#  [0. 1.]]   tower's query

The same matrix acts on every row. Different input vectors give different queries; we did not need a separate matrix per word. The token representation and its query are now different objects: the representation supplies the input, and the query is one transformed view used for matching.

ObjectShapeWhat varies?
Embedding tablevocabulary size × dOne learned row per token ID
Input Xn × dThe representations in this particular sequence
Query projection Wqd × d_kShared parameters, adjusted during training
Queries Q = X @ Wqn × d_kComputed for each position on this pass

At the first layer, the input includes embeddings and any positional signal. Deeper layers operate on representations already updated by earlier layers. The projection rule is shared; its inputs need not be static word vectors.

What is query/key space, and how does it relate to subspaces?

Query/key space is the coordinate space where a head compares queries with keys. If a head uses 128 coordinates for each, both live in R128\mathbb R^{128}, and their dot product produces a matching score there. The learned maps Wq and Wk translate the much wider token representations into these matching coordinates. Each head has its own maps; the coordinates need not have named semantic meanings.

For example, a 12,288-coordinate input can be multiplied by a 12,288 × 128 query matrix to produce a 128-coordinate query. The key matrix has the same shape and produces 128-coordinate keys from the source representations. The diagram uses bank and river to show this relationship; these widths illustrate a possible model configuration, rather than the sizes of our toy.

One head maps the full representation into a smaller matching space

Token representation space

12,288 dimensions in this example

Bank's full input representationA blue arrow stands for bank's vector with 12,288 coordinates. The three drawn axes are a schematic illustration of a much larger space.x_bank[x₁, x₂, …, x₁₂₂₈₈]

The current representation carries information available to this position.

Learned Wq12,288 × 128q = x @ WqEach output coordinate
reads all input coordinates.

Query/key matching space

128 dimensions in this example

Queries and keys are compared in a 128-coordinate matching spaceA yellow arrow represents bank's query. A green arrow represents river's key, computed separately with Wk. The two-dimensional grid is a schematic for 128 dimensions, and their illustrated alignment is hypothetical.q_bankk_river[q₁, q₂, …, q₁₂₈]

Wk separately maps source representations into keys with the same width.

q_bank = x_bank @ Wqk_river = x_river @ Wkscore = q_bank · k_river / √128
Example widths, not universal model settings. The drawn axes and arrow directions are schematic: they do not depict literal 3D and 2D spaces or measured semantic directions. The matching space is separate from the input space.

This connects directly to row space and null space: what the matrix reads and ignores. That article uses column vectors, while our code uses row vectors. To apply its notation, set M=Wq⊤M=W_q^\top, so q⊤=Mx⊤q^\top=Mx^\top. Now the same subspace picture applies:

  • In the 12,288-dimensional input space, the row space of MM contains the directions it reads. Its null space contains directions that contribute zero to the query. Adding a vector from this null space to the input leaves the query unchanged.
  • In the 128-dimensional output space, the column space of MM contains every query the linear map can produce. It may fill all of R128\mathbb R^{128} or be a smaller subspace if the map’s rank is below 128. See column space: which outputs can the matrix reach?.

The query map can read at most 128 independent input directions in this example, leaving at least 12,160 independent input directions in its null space. Those directions are ignored by this query map; the residual stream still carries the input, and other heads or projections can use information this map ignores. The input directions read by Wq and Wk can also differ, even though their outputs are compared using the same 128 matching coordinates.

The 128-dimensional matching space is therefore a separate output space, while the relevant input subspace lies inside the original 12,288-dimensional space. In attention, “projection” usually means a learned linear map: it can stretch and mix directions and is not generally an orthogonal projection onto a plane inside the original space.

What information does the query map lose?

Make this concrete with a separate 3D → 2D example. Using the column-vector notation above, choose:

M=Wq⊤=(101011),n=(11−1).M=W_q^\top= \begin{pmatrix}1&0&1\\0&1&1\end{pmatrix}, \qquad n=\begin{pmatrix}1\\1\\-1\end{pmatrix}.

This matrix reads the two sums x1+x3x_1+x_3 and x2+x3x_2+x_3. For the vector nn, both sums cancel:

Mn=(1+(−1)1+(−1))=(00).Mn= \begin{pmatrix}1+(-1)\\1+(-1)\end{pmatrix} =\begin{pmatrix}0\\0\end{pmatrix}.

So nn is in the null space. Adding any multiple of it leaves the query unchanged: M(e+tn)=Me+tMn=MeM(e+tn)=Me+tMn=Me. For example:

e=(123),e+n=(232),Me=M(e+n)=(45).e=\begin{pmatrix}1\\2\\3\end{pmatrix}, \qquad e+n=\begin{pmatrix}2\\3\\2\end{pmatrix}, \qquad Me=M(e+n)=\begin{pmatrix}4\\5\end{pmatrix}.

The query map cannot distinguish these two inputs. What it loses is the amount of input along the null-space direction (1,1,−1)(1,1,-1): increasing the first two coordinates together while decreasing the third by the same amount leaves both output sums unchanged. It does not simply discard the third coordinate; that coordinate contributes to both sums. The arithmetic identifies an ignored direction; identifying any semantic information encoded along it would require experiments on a trained model.

If the keys are held fixed, these identical queries also produce identical attention scores and weights. The input change could still affect this token’s key or value, another head, or the residual stream. “Lost” here means unavailable through this query map, rather than erased from the model’s entire representation.

One query can combine several matching criteria

One query can carry several matching preferences at once. If queries and keys have 128 coordinates, their scaled dot product is:

sij=qi⋅kj128=qi,1kj,1+qi,2kj,2+⋯+qi,128kj,128128.s_{ij}=\frac{q_i\cdot k_j}{\sqrt{128}} =\frac{q_{i,1}k_{j,1}+q_{i,2}k_{j,2}+\cdots+q_{i,128}k_{j,128}}{\sqrt{128}}.

All 128 coordinate pairs contribute to one compatibility score for this query/key pair. The query is compared with every allowed key, producing a separate score for each source position. Softmax converts those scores into one attention distribution. This is the scaled dot-product computation in the Transformer paper, §3.2.1.

128 coordinates does not mean 128 semantic features. A feature might correspond to a direction spread across many coordinates, and several features can use overlapping coordinates. Ten features could be distributed across 128 dimensions. A larger collection, such as 1,000 possible features, could in principle use overlapping directions, especially when only a few features are active at once. But 128 dimensions provide at most 128 linearly independent directions: they cannot independently preserve 1,000 arbitrary feature strengths through a linear encoding.

Toy Models of Superposition demonstrates how toy networks can represent more sparse features than dimensions, with interference between overlapping representations. This supports a representational possibility; it does not establish that a particular query contains 1,000 identifiable features or tell us what they are.

The dot product combines the evidence into one score, rather than returning a separate answer for every feature. Each head produces one attention distribution and one value mixture per receiving token. Multiple heads let different matching criteria produce separate distributions and mixtures; a single head can still combine several criteria within its own score.

Try three hypothetical matching directions

Return to the four-coordinate bank vectors, this time projecting into a three-dimensional query/key space. We hand-name its axes finance, water, and structure, and keep the key projection fixed. This is a new teaching construction, not a claim about the coordinates or behavior of a trained head.

Choose Find water evidence, Find finance evidence, or Find structure evidence to try a possible interpretation of bank’s query. Each preset changes the displayed Wq; the widget computes the resulting queries, keys, scores, and attention weights. Use Rotate view to inspect the three axes, or open Adjust Wq coefficients to combine or oppose directions.

What could this query match?

Receiving token: bank. Its input stays [0.50, 0.50, 0.80, 0.20]. Switch a hand-set Wq to change its matching direction.

Hypothetical interpretation: Which positions carry water-related evidence?

1. Produce the query

q_bank = x_bank @ Wq

Wq · 4 input coordinates × 3 matching coordinates
Inputfinancewaterstructure
finance0.000.000.00
water0.000.000.00
structure0.003.750.00
generic0.000.000.00

q_bank = 0.8 × [0.00, 3.75, 0.00]
= [0.00, 3.00, 0.00]

Only the structure row of this constructed Wq is nonzero. A real projection can combine every input coordinate.

Adjust Wq coefficients

Negative coefficients can make this query oppose a key direction. A zero query gives equal scores and uniform weights.

2. Compare query and keys

Query and keys in an illustrative three-dimensional matching spaceFinance, water, and structure are hand-chosen axes. Colored points are keys. The orange arrow shows bank's query direction; the gray point is absent from the selected sentence.financewaterstructurek_riverk_moneyk_bankk_the = 0q_bank

The query arrow is rescaled to show its direction. Scores use the full numerical query. Faded keys belong to words absent from this sentence.

3. Match first; mixing comes next

The same Wq produces a query for every token. Below, each key is computed using a fixed Wk that keeps finance, water, and structure and drops the generic coordinate.

TokenIts query x @ WqIts key x @ Wkq_bank · key / √3Bank's attention weight
the[0.00, 0.00, 0.00][0.00, 0.00, 0.00]0.000 11.1%
river[0.00, 0.00, 0.00][0.00, 1.00, 0.00]1.732 62.6%
bank[0.00, 3.00, 0.00][0.50, 0.50, 0.80]0.866 26.3%

The keys stay fixed when Wq changes. Switching sentences leaves bank's query unchanged here, but changes the available keys and their shares. The head would use these weights to mix values; this widget stops at matching.

These questions, axes, matrices, and token features are hand-designed teaching examples. Changing a preset changes Wq; it does not show a trained head choosing a new matrix for each sentence. Real heads learn numerical matching patterns jointly with key projections, without named semantic coordinates or literal questions.

Notice that the query need not identify a word’s intended sense on its own. In this construction, bank starts with the same input and produces the same query in both sentences. The available keys and values supply the differing context. Whether any of these hypothetical matching patterns helps a task is something training and evaluation must establish.

The embedding table and the projection matrices are separate

The embedding table E stores one starting vector per vocabulary token. The projection matrices Wq, Wk, and Wv transform a position’s current representation into its query, key, and value. Each head normally has its own projections in each transformer block; the token embedding table supplies the starting representations below the stack.

ObjectPurposeLearned or computed?
ESupplies the starting vector for each token IDLearned parameter table
Wq, Wk, WvTransform the current representationsLearned projection matrices
X, Q, K, VRepresent this input and its transformed viewsComputed on each forward pass
A, HWeight and mix the values for this inputComputed on each forward pass

How many tables and vectors are there?

The architecture below expands 2 heads processing 3 tokens in one standard self-attention sublayer. The embedding table is outside attention. Its selected rows become X; both heads read that same full input, using separate learned projection matrices. Within each head, Q, K, and V are computed matrices with one vector per token row. Follow the highlighted bank rows to count its queries, keys, and values across the two heads.

3 tokens × 2 heads · one standard self-attention sublayer

E · 1 token embedding table for the model

Vocabulary size × d · token IDs select rows here

the ethe = […]river eriver = […]money emoney = […]bank ebank = […]
↓ Look up the 3 token IDs once, before the stack

X · 1 input matrix · 3 × d

the xthe = etheriver xriver = eriverbank xbank = ebank
↓ The same full X feeds both heads in parallel

Head 1

3 learned projection matrices

Wq(1)d × d_k
Wk(1)d × d_k
Wv(1)d × d_v
↓ Multiply X by each projection

3 computed matrices · 3 token rows each

Q(1)3 × d_k
qthe(1) […]
qriver(1) […]
qbank(1) […]
K(1)3 × d_k
kthe(1) […]
kriver(1) […]
kbank(1) […]
V(1)3 × d_v
vthe(1) […]
vriver(1) […]
vbank(1) […]

Each token has 1 q + 1 k + 1 v in this head.

↓ Queries match keys; weights mix values
A(1) · 1 attention-weight matrix · 3 × 3A = softmax(Q @ Kᵀ / √d_k)H(1) · 1 mixture matrix · 3 × d_vH = A @ V

Head 2

3 learned projection matrices

Wq(2)d × d_k
Wk(2)d × d_k
Wv(2)d × d_v
↓ Multiply X by each projection

3 computed matrices · 3 token rows each

Q(2)3 × d_k
qthe(2) […]
qriver(2) […]
qbank(2) […]
K(2)3 × d_k
kthe(2) […]
kriver(2) […]
kbank(2) […]
V(2)3 × d_v
vthe(2) […]
vriver(2) […]
vbank(2) […]

Each token has 1 q + 1 k + 1 v in this head.

↓ Queries match keys; weights mix values
A(2) · 1 attention-weight matrix · 3 × 3A = softmax(Q @ Kᵀ / √d_k)H(2) · 1 mixture matrix · 3 × d_vH = A @ V
↓ Bring both heads' outputs together

Concat(H(1), H(2)) · 1 matrix · 3 × (2 d_v)

One row per token, containing both head mixtures

↓ Multiply by the block's output projection

Wo · 1 learned matrix for this sublayer · (2 d_v) × d

↓

Δ = Concat @ Wo · 1 attention-update matrix · 3 × d

Residual addition: Y = X + Δ · the original X is added here

↓ Feed-forward update → next block with its own projections
6 learned Q/K/V projection matrices6 computed Q/K/V matrices18 computed vectors: 6 queries + 6 keys + 6 values
Amber = learned parameters; blue = computed activations. The highlighted bank rows show 2 queries, 2 keys, and 2 values for that one token across 2 heads. In later blocks, X contains updated representations instead of fresh embedding lookups. Positions, normalization, and masks are omitted here. Counts describe logical matrices; implementations can pack several into one tensor.

For n token positions and h heads in one standard self-attention sublayer:

ObjectLearned or computed?CountShape of each matrix
Token embedding table ELearned1 for the model, before the stackvocabulary size × d
Input XComputed1 shared input to all heads in this sublayern × d
Query projections WqLearnedh, one per headd × d_k
Key projections WkLearnedh, one per headd × d_k
Value projections WvLearnedh, one per headd × d_v
Queries QComputedh matrices; n h query vectors totaln × d_k
Keys KComputedh matrices; n h key vectors totaln × d_k
Values VComputedh matrices; n h value vectors totaln × d_v
Query/key scores SComputedh, one per headn × n
Attention weights AComputedh, one per headn × n
Head mixtures HComputedh, one per headn × d_v
Concatenated head outputsComputed1n × (h d_v)
Output projection WoLearned1 for this sublayer(h d_v) × d
Attention update Δ and residual result YComputed1 of eachn × d

So one token has h queries, h keys, and h values in a block, while the block has 3h learned Q/K/V projection matrices and one output projection. A stack of N such blocks normally has its own projections in every block: 3Nh Q/K/V projection matrices and N output projections. These counts describe the standard multi-head arrangement, omit biases, and count logical matrices; implementations often pack several projections into a single parameter tensor.

Only the lookup is indexed by token ID. The projection matrices have input-coordinate rows and output-coordinate columns, so every token uses the same projection matrices within a head. Later blocks receive updated representations from the previous block, without repeating the embedding lookup. The block article adds the normalization and feed-forward details to this outline.

“Initial” describes where the lookup happens when processing a sentence. Before training from scratch, the embedding table usually starts with random numbers. Training learns those vectors jointly with the projections and the rest of the model; there is usually no separate stage that teaches the embeddings a “generic meaning” first. After training, the lookup retrieves a learned vector. It is context-independent because it does not inspect the current sentence. The same token ID retrieves the same row, even though later representations depend on context.

Our first examples use hand-set vectors so we can follow the arithmetic. The training example below learns the embedding table as well as the projections.

Three roles: matching and carrying information

The query is one of three transformed views:

ViewComputationJob
QueryQ = X @ WqRepresents what each receiving position matches against
KeyK = X @ WkRepresents each source position for that matching operation
ValueV = X @ WvSupplies the vectors that will actually be mixed
A communication analogy: queries and keys determine weights, values carry information, and the MLP processes each position independently.

Think of token positions as people exchanging messages. Query–key matching determines the weights; values supply the messages. These are continuous vector operations. The final panel previews the per-position MLP developed in the transformer-block article.

Queries and keys meet in Q @ K.T. Both sides can be learned: the model can transform how a receiver asks and how a source is found. The two projections need not be equal, so the score from i to j can differ from the score from j to i.

Values have a different job. Keep the queries and keys fixed and swap only the value coordinates:

A, H = attention(X2, swap, I2, swap)
print(A[-1].round(4))  # [0.2483 0.5035 0.2483] -- unchanged
print(H[-1].round(4))  # [0.5035 0.2483]        -- different content

Q and K control the mixture’s weights; V controls its content. A large weight on Eiffel does not by itself tell us which direction the output moves. We must also inspect Eiffel’s value. Our swap is hand-set to make that independence visible; training will learn the transformations together.

The full head has the familiar form H=softmax⁡(QK⊤/dk)VH=\operatorname{softmax}(QK^\top/\sqrt{d_k})V, as specified in the original Transformer paper, §3.2. The diagram follows its operations and dimensions, then shows the output projection used by the surrounding block:

Input vectors become queries, keys, and values; scores become attention weights, which mix values before an output projection.

W_Q, W_K, and W_V are learned projection matrices; Q, K, and V are computed activations. In the toy example the input is X. In a pre-norm block it is a normalized read, labeled U here. The output projection maps the head mixture back to model width; residual addition comes afterward.

d_v need not equal d. A head returns a mixture in value coordinates. The block’s output projection will map it into the coordinates of the representation we are updating.

Try the two-dimensional example below. Start with Raw query, switch to Swap query, then toggle Swap values. The numbers are computed from the matrices above; changing values leaves the weights untouched.

Change the match, then change the content

The receiving position is tower. Its input is [1, 0]. Keys stay unchanged; scores use √2 scaling.

the24.83%
Eiffel24.83%
tower50.35%
Input x[1.0000, 0.0000]
structure
Paris identity
−1.5 0 +1.5
Mixture h = A @ V[0.5035, 0.2483]
structure
Paris identity
−1.5 0 +1.5

Swapping the query changes the weights. Swapping values changes the mixture while leaving those weights untouched. No residual addition is shown here.

What the weight actually measures

A score compares one query/key pair. A weight compares that score with every other allowed score in the same row:

aij=exp⁡(sij)∑m allowedexp⁡(sim),sij=qikj⊤dk.a_{ij}=\frac{\exp(s_{ij})}{\sum_{m\ \mathrm{allowed}}\exp(s_{im})},\qquad s_{ij}=\frac{q_i k_j^\top}{\sqrt{d_k}}.

Adding another key can change a_ij without changing q_i or k_j. Likewise the weight is not a runtime word-co-occurrence count. Training changes the parameters; inference computes fresh scores and mixtures using those parameters. Position can also affect scores when positional information is supplied—we have deliberately left it out of this toy.

Dot products, cosine similarity, and scaling

We have used a dot product to compare each query with each key, then divided by dk\sqrt{d_k}. What does that division change? Compare three operations on the same vectors:

OperationFormulaWhat determines the score?
Dot productq⋅kq\cdot kDirections and vector lengths
Cosine similarityq⋅k∥q∥∥k∥\dfrac{q\cdot k}{\lVert q\rVert\lVert k\rVert}Angle only, for nonzero vectors
Scaled dot productq⋅kdk\dfrac{q\cdot k}{\sqrt{d_k}}Directions and lengths, with a fixed divisor for the head’s query/key dimension

A dot product includes vector length

For nonzero vectors, the dot product can be written as:

q⋅k=∥q∥∥k∥cos⁡θ.q\cdot k=\lVert q\rVert\lVert k\rVert\cos\theta.

It depends on both lengths and the angle between them. Take q=(1,0)q=(1,0) and k=(1,0)k=(1,0): their dot product is 11. Make both vectors ten times longer, giving q=(10,0)q=(10,0) and k=(10,0)k=(10,0), and the dot product becomes 100100. The directions have not changed.

Hold the angle fixed in the explorer and change either length. Then hold both lengths fixed and change the angle. The vectors are labelled aa and bb here, but the same arithmetic applies to a query and a key.

Rotate a. Watch its shadow.
Vector a projected onto the horizontal direction of bAt 20.00 degrees, a has length 10 and signed projection 9.40. Vector b has length 1. The dot product is 9.40. Vector b is drawn below the projection line for visibility, at the same scale.-10-50510ab

Blue: vector aGreen: projectionOrange: vector b

b points right and is drawn below the line for visibility. Both vectors use the same scale.

Signed projection9.40‖a‖ cos θ
Dot product9.40projection × ‖b‖
Projection vector(9.40, 0)on the horizontal line

The projection is positive: part or all of a points in the direction of b. Since b is a unit vector, the dot product equals the signed projection.

Length multiplies the signed angular contribution: making a vector longer makes a positive dot product more positive and a negative one more negative. Perpendicular vectors still give zero. A longer key therefore does not automatically receive a higher score.

Cosine similarity removes the lengths

Dividing by the product of the two lengths cancels those factors:

cosSim⁡(q,k)=q⋅k∥q∥∥k∥=cos⁡θ.\operatorname{cosSim}(q,k) =\frac{q\cdot k}{\lVert q\rVert\lVert k\rVert} =\cos\theta.

This is equivalent to taking the dot product after normalizing each vector to unit length. For nonzero vectors it lies between −1-1 and 11; it is undefined if either vector is zero.

Query qqKey kkDot productCosine similarity
(1,0)(1,0)(1,0)(1,0)1111
(10,0)(10,0)(10,0)(10,0)10010011
(1,0)(1,0)(0,1)(0,1)0000
(1,0)(1,0)(−1,0)(-1,0)−1-1−1-1

Scaling preserves the influence of lengths

In our head, dkd_k is the number of coordinates in each query and key. Every pair in that head uses the same divisor dk\sqrt{d_k}, regardless of the vectors’ actual lengths.

For example, a head with dk=64d_k=64 divides every dot product by 88:

Dot productScaled dot product
110.1250.125
8811
404055
10010012.512.5

These are illustrative scores for 64-dimensional vectors, separate from the two-dimensional examples above. Multiplying either vector by ten still multiplies its scaled dot product by ten. Dividing all scores by the same positive constant preserves their ordering and rescales their differences; it does not make the vectors unit length or bound the scores to [−1,1][-1,1].

Why divide by the square root of the dimension?

A dot product adds one product per coordinate:

q⋅k=∑r=1dkqrkr.q\cdot k=\sum_{r=1}^{d_k}q_r k_r.

To see how its scale can grow, assume all query and key components are independent random variables with mean zero and variance one. Each product then has mean zero and variance one:

E[qrkr]=0,Var⁡(qrkr)=E[qr2]E[kr2]=1.\mathbb E[q_r k_r]=0, \qquad \operatorname{Var}(q_r k_r) =\mathbb E[q_r^2]\mathbb E[k_r^2]=1.

Variances add for independent terms, so:

Var⁡(q⋅k)=dk,SD⁡(q⋅k)=dk.\operatorname{Var}(q\cdot k)=d_k, \qquad \operatorname{SD}(q\cdot k)=\sqrt{d_k}.

The expected dot product remains zero. What grows is its spread: positive and negative fluctuations become larger. At dk=64d_k=64, the variance is 6464 and the standard deviation is 88. Dividing by 88 brings the standard deviation back to 11 because dividing a random variable by a constant divides its variance by the constant’s square:

Var⁡ ⁣(q⋅kdk)=dk(dk)2=1.\operatorname{Var}\!\left(\frac{q\cdot k}{\sqrt{d_k}}\right) =\frac{d_k}{(\sqrt{d_k})^2}=1.

This is the motivation given in the original Transformer paper, §3.2.1. It explains the choice of divisor under simplifying assumptions; learned queries and keys need not remain independent or have unit component variance, so a trained head’s score variance is not guaranteed to be one.

Scaling changes how sharply softmax chooses

Consider one query with raw scores [4,2,0][4,2,0] against three keys in a four-dimensional head. Dividing by 4=2\sqrt{4}=2 gives [2,1,0][2,1,0]:

Scores sent to softmaxKey 1 weightKey 2 weightKey 3 weight
Raw: [4,2,0][4,2,0]86.7%11.7%1.6%
Scaled: [2,1,0][2,1,0]66.5%24.5%9.0%

Key 1 still wins, but receives a smaller share. Softmax responds to differences between scores: for two allowed keys, the weight ratio is a1/a2=exp⁡(s1−s2)a_1/a_2=\exp(s_1-s_2). Here scaling reduces that score gap from 22 to 11, changing the ratio from e2e^2 to ee. Adding the same constant to every score would leave the weights unchanged.

Large score gaps can make softmax nearly one-hot. Its derivatives with respect to its input scores then become small, which can weaken the learning signal through the attention weights. Scaling helps control this effect as query/key dimension increases. It does not prevent a trained head from learning sharp attention when useful.

Why not use cosine similarity instead?

Cosine similarity would change what the matching rule can use. Standard scaled dot-product attention retains the influence of both query/key directions and lengths. With keys fixed, increasing a query’s length by a positive factor scales its whole score row and can sharpen its attention distribution. Changing one key’s length affects that key’s score, with the effect depending on its sign.

Cosine-based attention is a possible alternative, but normalizing queries and keys removes these particular effects of their lengths. The fixed dk\sqrt{d_k} divisor instead adjusts the expected score scale as the head width changes.

Finally, keep vector normalization separate from weight normalization:

S=QK⊤dk→softmax across keysA.S=\frac{QK^\top}{\sqrt{d_k}} \quad\xrightarrow{\text{softmax across keys}}\quad A.

The divisor scales compatibility scores. Softmax turns each score row into nonnegative mixing weights that sum to one. Neither operation requires each query or key to have unit length.

Make it learn

Return to the bank task, but discard our hand-designed axes. The model now learns eight-dimensional embeddings and the three projection matrices. A classifier reads only the mixture at the final bank position and predicts water (0) or finance (1). It is a tiny supervised classifier, not a next-token language model or a complete transformer block.

Follow a sentence from lookup to an update

The widget uses a two-dimensional version of this classifier to keep every vector and parameter visible. Choose a sentence, then use Next stage to follow sentence → embedding lookup → Q/K/V → attention mixture → prediction → loss → gradients → parameter updates. At the gradient stage, the parameters have not changed yet. Apply update takes one SGD step; Next sentence continues with the updated parameters and the other example.

Compare the embedding table with the projection matrices, then compare those stored parameters with the vectors computed from them. The gradient and update stages show all parameter groups together, including the embedding row that received no gradient because its word was absent. Use Train 20 steps to alternate both examples, or Reset training to return to the same random initialization.

Follow one sentence through training

One head, two learned coordinates, two classes. All numbers are computed. Coordinates have no hand-assigned meaning.

Completed updates: 0. SGD learning rate: 0.3. Extra training alternates the two examples and keeps the same shared parameters.

  1. 1. Sentence
  2. 2. Lookup
  3. 3. Q / K / V
  4. 4. Mixture
  5. 5. Prediction
  6. 6. Loss
  7. 7. Gradients
  8. 8. Update

1. Sentence

theID 0riverID 1bankID 3

Target: water. The classifier will read only the mixture at bank. The target is used by the loss, not by the attention calculation.

The parameter tables start with repeatable random numbers; the classifier bias starts at zero. Nothing has been trained separately beforehand.

This is a two-example supervised training demonstration, not a generalization test or a complete transformer block. It omits positions, normalization, residual additions, and the FFN. The Python model below uses eight coordinates and Adam; this widget uses two coordinates and SGD.

The runnable eight-dimensional model

import torch
import torch.nn as nn

vocab = {"the": 0, "river": 1, "money": 2, "bank": 3}
data = [(["the", "river", "bank"], 0),
        (["the", "money", "bank"], 1),
        (["river", "bank"], 0),
        (["money", "bank"], 1)]

def encode(tokens):
    return torch.tensor([vocab[t] for t in tokens])

class OneHeadSense(nn.Module):
    def __init__(self, vocab_size, d=8):
        super().__init__()
        self.emb = nn.Embedding(vocab_size, d)
        self.Wq, self.Wk, self.Wv = (
            nn.Linear(d, d, bias=False) for _ in range(3))
        self.cls = nn.Linear(d, 2)
        self.d = d

    def forward(self, ids, return_attn=False):
        X = self.emb(ids)
        Q, K, V = self.Wq(X), self.Wk(X), self.Wv(X)
        A = torch.softmax(Q @ K.T / self.d**0.5, dim=-1)
        H = A @ V
        logits = self.cls(H[-1])
        return (logits, A) if return_attn else logits

torch.manual_seed(0)
model = OneHeadSense(len(vocab))
opt = torch.optim.Adam(model.parameters(), lr=0.05)
loss_fn = nn.CrossEntropyLoss()
for step in range(300):
    for tokens, label in data:
        logits = model(encode(tokens)).unsqueeze(0)
        loss = loss_fn(logits, torch.tensor([label]))
        opt.zero_grad()
        loss.backward()
        opt.step()

for tokens, _ in data[:2]:
    logits, A = model(encode(tokens), return_attn=True)
    print(tokens, "prediction:", logits.argmax().item())
    print({t: round(w, 2) for t, w in zip(tokens, A[-1].tolist())})

In the checked seed-0 CPU run, the predictions are 0 and 1, and the rounded attention rows are:

{'the': 0.0, 'river': 1.0, 'bank': 0.0}
{'the': 0.0, 'money': 1.0, 'bank': 0.0}

This run learned strongly selective routing. That is an observed solution, not something the four-example task forces: a uniform mixture also contains enough information to classify the examples. The printed 0.0 values are rounded, not proof of exactly zero softmax weights. There is no held-out evaluation here; this demonstrates fitting a tiny task.

What changed during training? The embedding table, Q/K/V projections, and classifier: 4×8 + 3×8×8 + (8×2+2) = 242 parameters. The weights A are activations, recomputed from each input, not another table of trainable parameters. Training adjusts the fixed transformations so their input-dependent mixtures help the classifier lower its loss.

Heads and blocks train together

The toy has one head. In standard full-model transformer training, all attention heads in all transformer blocks train together. A forward pass computes predictions and a loss. Backpropagation carries gradients from that loss through the blocks, through each head’s query/key and value routes, and back to the embedding table. The optimizer then updates all trainable parameter groups, including the output projections and FFNs. Each parameter receives its own gradient; this does not require every scalar to change on every step.

There is no separate training loop for each head. Heads within one block compute in parallel, while blocks pass updated representations forward sequentially. Backpropagation follows those dependencies in reverse. Our loop’s model.parameters() includes the embeddings, three projections, and classifier together; a full transformer’s optimizer likewise includes the parameters across the whole trainable model.

Can two small matrices recover identifiable features?

The standalone feature-recovery experiment trains only two 9 × 3 query and value projections on synthetic data. It tests which known features the maps recover, checks held-out predictions, and intervenes on relevant and irrelevant input directions. Fixed keys and output targets deliberately anchor the feature meanings so they can be identified.

Attention learns the data, not the world

The companion train_biased_dataset.py assigns arbitrary labels to six context words, including opposite labels for lawyer and attorney. In a checked seed-0 run, it fits those assignments:

ContextAssigned labelPredictionRounded attention from bank to context
riverwaterwater0.00
moneyfinancefinance1.00
vaultwaterwater0.00
salmonfinancefinance1.00
lawyerwaterwater0.00
attorneyfinancefinance1.00

The result is more interesting than “the model always attends to the clue.” It solves the two classes with different attention patterns. Very small weights, other values, and the classifier all matter; a rounded heatmap alone is not an explanation of the prediction.

The narrow conclusion is that this supervised toy can learn arbitrary label assignments. It does not establish that a language model reproduces every bias in its data or cannot learn useful generalizations. To study those questions we would need a different experiment and evaluation.

Causal masking: a decoder only looks left

So far every position can read every other position. During left-to-right language-model training, the representation used to predict the next token must not see that future token. A causal mask changes which source positions are allowed.

The function already includes the necessary step, just before softmax:

future = np.triu(np.ones(scores.shape, dtype=bool), k=1)
scores = np.where(future, -np.inf, scores)

Exponentiating negative infinity gives zero, so the forbidden positions get zero weight. Setting their scores to zero would not work: exp(0) = 1 would still give them a share. Every row retains at least its own position.

A five-token score matrix is masked above the diagonal, then row-wise softmax turns the remaining scores into probabilities.

Follow the same illustrative scores across all three panels. Masked entries become zero probability; every row is normalized over its allowed keys. Values are rounded for display.

The toggle below compares the resulting access patterns for another sentence:

Self-attention weights — “the tired cat finally slept”
key — “…attends to this token”
the
tired
cat
finally
slept
the
0.55
0.05
0.30
0.03
0.07
tired
0.05
0.45
0.40
0.03
0.07
cat
0.18
0.30
0.40
0.04
0.08
finally
0.04
0.05
0.10
0.41
0.40
slept
0.05
0.20
0.35
0.20
0.20
query — “this token attends…”

Each row is one token's attention distribution, produced bysoftmax(Q·Kᵀ / √d_k) and summing to 1; the output for that token is the weighted sum of every value vector using these weights. Switch toCausal and the upper triangle is masked to −∞ before the softmax, so a token can attend only to itself and the tokens before it — the surviving weights renormalize. The mask distinguishes bidirectional from causal attention. Implementations also differ in details such as normalization placement. Weights here are illustrative, chosen to show the shape of a real head, not dumped from a model.

For “the river bank,” the last row is unchanged: bank can already see the entire sequence. For “the bank river,” causal bank cannot receive anything from the future river position. The source restriction changed; the projection-and-mixing mechanism did not.

A mask specifies access, not a full positional encoding. Our position-free toy still has limitations; the block article explains where positional information enters. For generation, previously computed keys and values can be reused under causal attention—the KV-cache article follows that consequence. Cross-attention instead takes its queries and its key/value pairs from different sequences.

Watch real heads

The hand-set axes made arithmetic readable. In a trained model, inspect both where information is gathered and what contribution is produced.

BertViz visualizes attention patterns head by head. AttentionViz additionally supports exploring query/key geometry and patterns across inputs. exBERT is another exploration interface. These are companion tools, not evidence that a particular head must reproduce our toy.

Try two sentences containing different uses of bank. Inspect several heads and layers, then compare the corresponding hidden representations. A heatmap displays mixing coefficients; the values and output projection determine the vector actually contributed. Treat a proposed role such as “disambiguation head” as a hypothesis to test across inputs and with interventions.

Where this leaves us

We built a contribution in three stages: match, weight, mix. The initial bank experiment showed information flowing through a weighted sum. The Eiffel example separated matching from raw input similarity, and changing values showed that the same weights can carry different content. Training supplied the parameters instead of our hand choosing them.

The next step is to incorporate that contribution. In the simplest same-width case with identity output projection, tower = [1, 0] plus its swap-query mixture [0.2483, 0.5035] gives [1.2483, 0.5035]. The mixture is one contribution to an existing representation. A real block handles the output projection, multiple heads, normalization, and another update from the FFN.

Inside a transformer block follows those updates through the complete computation.

Optional: the mathematics behind the head

Theoretical and mathematical foundations

Learned matching, kernel smoothing, and memory

Substituting the projections into an unscaled score gives sij=XiMXj⊤s_{ij}=X_i M X_j^\top, with M=WQWK⊤M=W_QW_K^\top. Its rank is at most dkd_k, and it need not be symmetric. This is a bilinear compatibility function, not generally a mathematical distance metric. The key distinction from raw similarity is the learned matrix between the two inputs.

The factorization is not unique: replacing WQW_Q by WQRW_Q R and WKW_K by WKR−⊤W_K R^{-\top} preserves the scores for invertible RR. This statement describes the plain content-only score; positional operations inserted between projections and scoring need separate treatment. Caching keys does not make the factors mathematically identifiable.

The normalized weighted sum also connects to kernel smoothing: use positive compatibility exp⁡(qikj⊤/dk)\exp(q_i k_j^\top/\sqrt{d_k}), normalize across sources, and average their values. Transformer Dissection develops this perspective. Hopfield Networks is All You Need connects an attention-style update to associative-memory retrieval. These are additional views of the operation, not prerequisites for running the toy.

There is a useful different mechanism if we remove softmax. With column vectors and constant scaling omitted, unnormalized causal linear attention can be rearranged as:

yi=∑j≤i(qi⊤kj)vj=(∑j≤ivjkj⊤)qi=Siqi.y_i=\sum_{j\leq i}(q_i^\top k_j)v_j =\left(\sum_{j\leq i}v_j k_j^\top\right)q_i=S_iq_i.

Update the memory by Si=Si−1+viki⊤S_i=S_{i-1}+v_i k_i^\top and read it with qiq_i. The memory has fixed dimensions for fixed key/value widths. Linear Transformers Are Secretly Fast Weight Programmers relates this mechanism to earlier fast-weight systems. Standard softmax attention does not admit this rearrangement; removing softmax changes the operation.

Reverse-engineer a head you trained

A further experiment: repeated patterns and ablation

The companion examples/train_induction_head.py trains a two-layer attention-only model on repeated sequences, then measures attention patterns and ablates heads. This is a separate lab, beyond the one-head sense classifier. An induction pattern continues A B … A with B; the experiment tests whether the model discovers a mechanism useful for that task.

Run the script, record the loss and its induction scores, and compare loss before and after each ablation. Treat the existence and location of a particular circuit as a result to check, not as something guaranteed by the architecture or random seed. The script uses plain PyTorch; TransformerLens supplies hooks and activation access for more extensive investigations.

For a guided investigation, ARENA’s Introduction to Mechanistic Interpretability walks through finding induction heads, inspecting their QK and OV circuits, and testing their role with ablations using TransformerLens.

For a worked example before inspecting a trained model, skip-trigram bugs and induction heads shows why different queries can receive the same unwanted message, then builds a circuit that finds a continuation by matching the token that preceded it.

A name such as “coreference head” or “induction head” should summarize measured behavior. Correlation with a pattern suggests a role; intervention tests what depends on it. Even an ablation must be interpreted with care, since other components can interact with or compensate for the removed head.

Image viewer

Pinch or use + / − to zoom. Drag or use arrow keys to move. Esc to close.