Circuits, explained · Part 3 of 7 · Covers Conceptual take-aways, One-Layer Attention-Only Transformers, The Path Expansion Trick, OV and QK Independence, Path Expansion of Logits
Paths Through the Model: the Path Expansion Trick
How the paper turns a one-layer model into a sum of end-to-end paths, one for the token itself and one for each head. We expand the real model, freeze its attention, and check every term.
A Mathematical Framework for Transformer Circuits. Nelson Elhage, Neel Nanda, Catherine Olsson, Tom Henighan, Nicholas Joseph, Ben Mann, Amanda Askell, Yuntao Bai, Anna Chen, Tom Conerly, Nova DasSarma, Dawn Drain, Deep Ganguli, Zac Hatfield-Dodds, Danny Hernandez, Andy Jones, Jackson Kernion, Liane Lovitt, Kamal Ndousse, Dario Amodei, Tom Brown, Jack Clark, Jared Kaplan, Sam McCandlish, Chris Olah. Transformer Circuits Thread, 2021.
Parts 1 and 2 took the model apart piece by piece: the residual stream, then one attention head. This part puts the pieces back together in a new shape: a sum of paths from the input tokens to the output. Every path can be read on its own.
We use the same one-layer model, attn-only-1l, and the same prompt as Part 2: "I like green tea, so I drink green". The model's next word is " tea", with probability 0.897. By the end of this part we will know exactly how much each path adds to that guess.
The section on one-layer models opens with a claim, and then says what it will do with it.
"Ensemble" is a strong word. It says that the one-layer model is nothing more than its members, added together. To check this, we need to write the model in a form where the sum is visible. That form is the path expansion.
Before the trick, the paper writes the one-layer model again, in three lines.
Here t is the whole prompt as one table of one-hot vectors, one column per position. So x0 and x1 are tables too, with one stream vector per position. Each head works on the whole table, because it reads across positions.
Is this product really the model? We computed it by hand, stage by stage, at all ten positions of our prompt:
python
x0 = E + P # (Id ⊗ W_E) t, plus the position embeddingmid = x0 + sum(A[h] @ (x0 / s1[:, None]) @ OV[h] for h in range(8)) + bias_O # (Id + Σ_h A^h ⊗ W_OV^h) x0prod = (mid / sf[:, None]) @ W_U + b_U # (Id ⊗ W_U) x1
plain text
== 1. THE ONE-LAYER MODEL AS A PRODUCT OF THREE TERMS (attn-only-1l) prompt: ['<|BOS|>', 'I', ' like', ' green', ' tea', ',', ' so', ' I', ' drink', ' green'] (10 tokens); the model predicts ' tea' next embed -> (Id + sum of 8 heads) -> unembed, by hand at all 10 positions: |by hand - model logits| max = 2.6e-05 layer-norm scales at the last position: before attention 0.238, before the unembedding 0.493
The largest difference over all 48,262 logits at all ten positions is 0.000026, which is rounding noise. Three things in the code are not in the paper's formula. The real model adds a position embeddingP to each token's vector. It has biases (bias_O, b_U). And it has the leftover layer norm from Part 1, which divides each vector by a scale (s1, sf).
We read the layer-norm scales from the real run and then hold them fixed. With fixed scales, a division is just a multiplication by a number, so it fits inside the product. The scales at the last position are 0.238 and 0.493. The paper's models have no layer norm, so it does not need this step.
Now comes the trick. It is the oldest move in algebra: multiply out the brackets.
Take a product of two sums of numbers, (1+2)×(1+3). You can add first and then multiply: 3×4=12. Or you can multiply every part of the first bracket by every part of the second and add the results: 1+2+3+2×3=12. The answer is the same. The second way keeps a record of where each part of the answer came from.
The middle factor is a sum of nine parts: Id and one term per head. The outer factors have one part each. So the expansion has 1×9×1=9 products. Take the first one, with Id from the middle:
(Id⊗WU)⋅Id⋅(Id⊗WE)=(Id⋅Id)⊗(WUWE)=Id⊗WUWE
The middle Id changes nothing, so it disappears. The second line is the mixed-product property from Part 2: (A⊗B)(C⊗D)=(AC)⊗(BD). The left sides of the two factors multiply, and the right sides multiply. Now take one head h from the middle:
The same rule puts everything that acts on positions on the left of ⊗ and everything that acts on vectors on the right. On the left, only the pattern Ah is left. On the right, the chain of matrices runs from the token to the logits. Add the nine products, and you get the paper's sum.
Read each symbol once more. Id⊗WUWE is the direct path: no mixing of positions, and the table WUWE from Part 1. Ah⊗WUWOVhWE is the head term of head h. The pattern Ah says which positions are read. The table WUWOVhWE says what reading each token does to the logits. That table is the OV circuit of Part 2.
Our toy model has three tokens, a, b and c, a stream of 2 numbers and one head. The embedding puts a at (1,0), b at (0,1) and c at (1,1). The unembedding reads the logit of a from number 1 and the logit of b from number 2, and c always gets 0. The head swaps the two numbers. On the text "a b", position 2 reads both tokens, half each.
The toy model on the text "a b", at position 2. Left, the product, layer after layer: the embedding gives (0, 1), the head adds (0.5, 0.5), so the stream is (0.5, 1.5), and the unembedding gives the logits (0.5, 1.5, 0). Right, the sum of paths: the direct path gives (0, 1, 0) and the head term gives (0.5, 0.5, 0). Their sum is the same (0.5, 1.5, 0).
Work the head term out by hand. The OV table WUWOVWE has one column per source token. Reading a gives (0,1,0), because the head swaps (1,0) to (0,1), which is the logit of b. Reading b gives (1,0,0). Position 2 takes half of each column: 0.5×(0,1,0)+0.5×(1,0,0)=(0.5,0.5,0). The script prints the same numbers:
plain text
W_U W_OV W_E (OV circuit, column = source token) = [[0.0, 1.0, 1.0], [1.0, 0.0, 1.0], [0.0, 0.0, 0.0]] direct path term = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]; head term = [[0.0, 1.0, 0.0], [0.5, 0.5, 0.0]]; sum = [[1.0, 1.0, 0.0], [0.5, 1.5, 0.0]] as one 6 x 6 matrix (Id (x) W_U W_E + A (x) W_U W_OV W_E) times the flattened tokens [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]: [1.0, 1.0, 0.0, 0.5, 1.5, 0.0]
The last line writes the whole toy model, with A fixed, as one 6 × 6 matrix. It acts on the two one-hot tokens laid end to end. With A fixed, the model really is one matrix times the tokens.
Now the real model. At the last position, the one that predicts " tea", we computed every path term over all 48,262 logits. Each head term is computed in tokens, as the formula says: the pattern of the head times the token columns of its OV table.
python
terms['direct path (token)'] = (E[i] / sf[i]) @ W_U # Id ⊗ W_U W_Efor h in range(8): terms[f'head 0.{h} (tokens)'] = (A[h, i] @ (E / s1[:, None]) @ OV[h] / sf[i]) @ W_U # A^h ⊗ W_U W_OV^h W_E
plain text
== 2. EXPAND THE PRODUCT: A SUM OF END-TO-END PATHS (logits at the last position, " green") 12 terms: the direct path, 8 head terms (the paper's 9 terms), and 3 parts the paper leaves out |sum of the terms - model logits| max over all 48,262 logits = 2.1e-05 term adds to logit of tea tokens it pushes up most direct path (token) +5.501 ['ery', 'houses', 'belt', 'wich'] head 0.0 (tokens) +1.321 [' moderation', 'avour', 'mented', ' replen'] head 0.1 (tokens) -1.466 ['ated', 'ided', 'rices', 'istant'] head 0.2 (tokens) +1.545 [' moderation', ' wouldn', ' gotta', '))))'] head 0.3 (tokens) +5.465 [' tea', ' Tea', ' teac', ' brew'] head 0.4 (tokens) +0.012 ['\n\n\n ', '\n\n\n\n', '\n ', '\n\n '] head 0.5 (tokens) +1.146 ['MENT', 'MENTS', 'vent', ' WARRANTIES'] head 0.6 (tokens) +0.317 [')],', '”).', ']),', '”)'] head 0.7 (tokens) +3.353 [' vinegar', ' snacks', ' cigarettes', ' candles'] direct path (position) +0.653 [' yang', '”),', '?”', '”).'] heads (positions) -1.215 ['aliana', 'gmail', 'visors', 'vironments'] biases b_O and b_U +3.495 [',', '.', ' and', ' in'] sum +20.128 (model logit of " tea": 20.128)
How much each path of attn-only-1l adds to the logit of "tea" at the last "green". The direct path adds +5.501 and head 0.3 adds +5.465; head 0.7 adds +3.353 and the biases +3.495; head 0.1 takes away 1.466. The first nine rows are the paper's terms, and all twelve add up to 20.128, the model's real logit.
The twelve terms add up to the model's logits. The largest error over all 48,262 logits is 0.000021. So the expansion is not an approximation. It is the model, written another way.
Now the table can be read. Two paths carry most of the vote for " tea". The direct path adds +5.501, and head 0.3 adds +5.465. Head 0.7 adds +3.353, and head 0.1 votes against with −1.466. The other heads add a little.
The last three rows are parts the paper leaves out: the position embedding, through the direct path and through the heads, and the biases. They do not depend on which tokens are in the prompt, only on where they are. That is why the paper can drop them and still describe what the model does with tokens.
Look at the top tokens of the direct path at the last " green": "ery", "houses", "belt" and "wich". These pieces complete "greenery", "greenhouses", "greenbelt" and "Greenwich". That is bigram knowledge in its purest form: it only needs the current token.
But the direct path also adds +5.501 to " tea". "Green tea" is a common phrase, so " tea" after " green" is a good bigram too. Without the head, this is the only reason the model would think of tea. The head adds a second, separate reason: "tea" appeared earlier in this text.
The section after the trick starts from one head term and asks what it means.
What does Ah⊗WUWOVhWE do at one position i? Apply it to the prompt t, and read row i. The result is a sum over the source positions j:
[(Ah⊗WUWOVhWE)t]i=j≤i∑Aijh(WUWOVhWE)tj
Read it left to right. The head term at position i is a weighted sum. Each source position j gives one column of the OV table: the column of its own token tj. The weight of that column is Aijh, how much position i reads position j.
In the toy, position 2 had weights 0.5 and 0.5 on a and b. So its head term was half of column a plus half of column b. On the real model, here is head 0.3 at the last " green", for the logit of " tea":
plain text
== 3. ONE HEAD TERM IN TOKENS: A^h (x) W_U W_OV^h W_E (head 0.3, destination = last " green") each source token j adds A[last, j] x (column t_j of the OV circuit) to the logits; its effect on " tea": j=0 '<|BOS|>' weight 0.366 x column value -0.456 = -0.167 j=1 'I' weight 0.003 x column value +3.673 = +0.011 j=2 ' like' weight 0.001 x column value -6.281 = -0.008 j=3 ' green' weight 0.013 x column value +6.110 = +0.077 j=4 ' tea' weight 0.370 x column value +12.150 = +4.497 j=5 ',' weight 0.087 x column value +0.040 = +0.003 j=6 ' so' weight 0.001 x column value +2.496 = +0.001 j=7 ' I' weight 0.002 x column value +1.022 = +0.002 j=8 ' drink' weight 0.147 x column value +6.649 = +0.979 j=9 ' green' weight 0.011 x column value +6.281 = +0.070 sum = +5.465 (term "head 0.3 (tokens)" above: +5.465)
Head 0.3 at the last "green", taken apart by source token. For each token: the attention weight, the value of that token's OV column in the row for "tea", and their product. "Tea" has weight 0.370 and column value +12.15, so it adds +4.497. "Drink" adds +0.979 and BOS takes away 0.167. The total, +5.465, is the head 0.3 term.
Almost all of the head's vote comes from one source: " tea" at position 4. Its column has the value +12.15 in the row for " tea", the largest of all ten sources. That is the copying behaviour of Part 2: reading " tea" pushes up " tea". The weight 0.370 decides how much of that column arrives.
The two rows for " green" have different column values, +6.110 and +6.281. The token is the same, but the layer-norm scales of the two positions differ a little, because their position embeddings differ. Our columns include the scales. In the paper's model, with no layer norm, the two columns would be equal.
The paper then explains why the two halves of the head term can be thought about apart.
We changed one token of the prompt, " drink", to " want", and looked at the last row of head 0.3 again:
plain text
== 5. ONE ROW OF THE PATTERN DEPENDS ON EVERY SOURCE TOKEN (softmax), head 0.3, last row before: "I like green tea, so I drink green" weight on " tea" 0.370, on BOS 0.366, on token 8 ' drink' 0.147 after : "I like green tea, so I want green" weight on " tea" 0.434, on BOS 0.429, on token 8 ' want' 0.000 the raw score of " tea" is the same in both runs: 2.929 and 2.929
The last row of head 0.3's pattern, before and after one token changes. Before, BOS and "tea" each get about 0.37 and "drink" gets 0.15. After "drink" becomes "want", the head gives "want" no weight, and BOS and "tea" rise to 0.43 each. The score of "tea" did not change, but the softmax shares out the weight that "want" left free.
The score between the last " green" and " tea" did not change: 2.929 in both runs. Only the token at position 8 changed. Yet the weight on " tea" rose from 0.370 to 0.434. The new token " want" took no weight, and the softmax gave its share to the others. This is what the footnote means: the QK circuit gives each pair a score, but the weight of a pair depends on the whole row.
The head term has one non-linear piece: the pattern Ah, which comes from a softmax of the tokens. The paper now offers a way to remove even that.
What does "linear in the tokens" promise? A linear map (Part 1) keeps sums and scales. The output for a+b is the output for a plus the output for b. The output for 2a is twice the output for a. The toy shows it. We keep A fixed and swap the first token from a to c:
plain text
== C. FREEZING THE PATTERN "c b" with A frozen: logits [[2.0, 2.0, 0.0], [1.0, 1.5, 0.0]]; change at position 2 = [0.5, 0.0, 0.0] A[2,1] x (OV column c - OV column a) = 0.5 x ([1.0, 1.0, 0.0] - [0.0, 1.0, 0.0]) = [0.5, 0.0, 0.0] linear: T("a b" + "c b") = [3.0, 3.0, 0.0, 1.5, 3.0, 0.0]; T("a b") + T("c b") = [3.0, 3.0, 0.0, 1.5, 3.0, 0.0]
The change at position 2 is exactly the weight, 0.5, times the difference between two OV columns. You can check it with a pencil: column c is (1,1,0), column a is (0,1,0), so the change is (0.5,0,0).
On the real model we froze the patterns of all eight heads, recorded on our prompt. We also froze the layer-norm scales, because the paper's model has no layer norm. Then we fed in token embeddings that are not real text: the sum of two prompts, and twice one prompt.
We call g(t) the part of the logits that depends on the tokens. It is the logits for t minus the logits with every token embedding set to zero. The zero run keeps the positions and biases, so subtracting it leaves only the token paths. Prompt a is ours, and prompt b is "The cat sat on the mat and the dog", also ten tokens.
python
frozen = {'blocks.0.attn.hook_pattern': ..., 'blocks.0.ln1.hook_scale': ..., 'ln_final.hook_scale': ...} # from run 1g = lambda e: run(e, freeze) - run(zero, freeze) # run 2: replace the token embeddings, reuse the patterns
plain text
== 4. FREEZE THE ATTENTION PATTERNS: THE LOGITS BECOME LINEAR IN THE TOKENS frozen : g(a + b) vs g(a) + g(b): max diff 4.4e-05; g(2a) vs 2 g(a): max diff 5.4e-05 (typical |g(a)| = 1.73) not frozen: g(a + b) vs g(a) + g(b): max diff 1.7e+01; g(2a) vs 2 g(a): max diff 2.7e+01 (typical |g(a)| = 2.14)
The freezing trick as two runs. Run 1 on real text records the pattern of every head; that run uses only the QK circuits. Run 2 feeds in any tokens and reuses the recorded patterns, so the logits become linear in the tokens. The test on attn-only-1l: with frozen patterns, g(a + b) and g(a) + g(b) differ by at most 0.000044; with patterns computed again, by up to 17.
With frozen patterns, the largest error is 0.000044, while a typical logit part is 1.73. That is rounding noise: the frozen model is exactly linear. Without freezing, the error reaches 17 for sums and 27 for doubling, larger than the logits themselves. The only difference between the two rows is the softmax of the patterns.
Now a more useful use of the trick. We swap one source token, " tea" at position 4, for " coffee", with the patterns frozen. The formula above predicts the change at the last position: for each head, the weight on position 4 times the difference of two OV columns.
plain text
swap source token 4 " tea" -> " coffee", patterns and scales frozen, watch the last position: change of all logits = sum over heads of A[last, 4] x (OV column coffee - OV column tea): max diff 2.9e-05 frozen run: logit of " tea" changes by -2.361, logit of " coffee" by +2.739 the real model on "I like green coffee, so I drink green" predicts: [(' coffee', 0.562), (' tea', 0.241), (' and', 0.015)]
The prediction from the OV columns matches the frozen run over all 48,262 logits, to 0.000029. The swap lowers " tea" by 2.361 and raises " coffee" by 2.739. So, with the reading fixed, what the heads move depends only on which token they read.
The real model, with its patterns free, agrees in direction: it now predicts " coffee" with probability 0.562, and " tea" falls to 0.241. The direct path still sees only " green", so its vote for " tea" is unchanged. That is one reason tea keeps some probability.
What the frozen test cannot show. A frozen run equals the real model only on the text that made the patterns. On new text the real patterns change, and the frozen answer can be wrong. We also froze the layer-norm scales; the real model computes them again. The trick is a tool for reading the OV side, not a replacement for the model.
The trick does not stop at one layer. The two-layer section of the paper applies it again, and the result is the map for Parts 5 and 6.
Count the paths like the brackets in (1+2)(1+3). Each middle bracket has 1+8 parts, so there are 9×9=81 products. The toy script counts them for a few head numbers:
plain text
== D. COUNTING PATHS 8 heads per layer: one layer 9 terms; two layers 81 terms (1 + 16 + 64) 12 heads per layer: one layer 13 terms; two layers 169 terms (1 + 24 + 144)
So the two-layer model has 1 direct path, 16 single-head terms and 64 virtual-head terms. The paper's numbering of layers starts at 1; TransformerLens names the same layers 0 and 1.
There is one catch, and a footnote of the freezing section flags it. In models with more than one layer, the QK circuit "can be more complicated". A layer-2 head computes its pattern from the stream, and the stream already holds what layer-1 heads wrote. So the layer-2 patterns depend on layer 1. This is Q- and K-composition, and the induction heads of Part 6 are built from it. With frozen patterns the expansion above is still exact; the patterns themselves are what need explaining.
We expanded attn-only-2l, a two-layer model with eight heads per layer, on the same prompt. With frozen patterns and scales, we computed all 81 path terms, plus the position and bias parts, and grouped them:
plain text
== 6. TWO LAYERS IN OUTLINE (attn-only-2l): 1 + 16 + 64 PATH TERMS same prompt; attn-only-2l predicts ' tea' after the last " green" terms: direct path 1, layer-0 heads 8, layer-1 heads 8, virtual heads 64, positions and biases 92 (paper's terms: 1 + 16 + 64 = 81) |sum of all terms - model logits| max = 2.7e-05 group adds to logit of ' tea' spread of the 48,262 logits (std) direct path +3.068 0.907 layer-0 heads +0.376 0.881 layer-1 heads +15.291 2.002 virtual heads +1.061 1.082 positions and biases +2.962 2.933 sum +22.758 (model logit 22.758, probability 0.986)
The logit of "tea" in attn-only-2l at the last "green", split into groups of paths. The eight layer-1 heads add +15.291, the direct path +3.068, the 64 virtual heads together +1.061, the layer-0 heads +0.376, and the positions and biases +2.962. The groups add up to the model's logit, 22.758.
The sum again matches the model, to 0.000027. On this prompt, the second-layer heads carry most of the vote for " tea": +15.291 of 22.758. The 64 virtual heads together add +1.061, less than a tenth of what the layer-1 heads add. That fits the paper's remark, but one prompt cannot test it. Part 5 does the real measurement, and Part 6 shows which second-layer heads do the work and why.
Next, in Part 4: we read the head terms of the one-layer model. Each head's QK and OV tables become lists of skip-trigrams. The paper shows copying, simple in-context learning, and the bugs that this design cannot avoid.
Run it yourself
The two scripts behind this part are circuits_part3.py (every real-model number: the product by hand, the expanded terms, one head term by source token, the freezing tests, the softmax row, and the two-layer outline) and circuits_part3_toy.py (the hand-sized examples). They use attn-only-1l and attn-only-2l and run on the CPU.