The logit lens reads what a language model is predicting partway through its computation. A Transformer builds its next-token prediction over many layers, but it reads that prediction out only once, at the very end. The lens applies the same readout after every layer, so you can watch the prediction form. Its companion, direct logit attribution, splits the final prediction exactly into one contribution from each attention head, each MLP, and the embeddings. Together they are the cheapest first look inside a model, and often the first thing a researcher runs on a new one.
Keep one example in mind. GPT-2 reads "The Eiffel Tower is in the city of" and puts most of its probability on " Paris". At which layer did " Paris" become the answer, and which heads and MLPs put it there? This section builds the two tools that answer those questions. It says exactly what each one measures and shows where each one stops being trustworthy.
The tools also address a weakness of the previous section. Section 23.2 read activations with probes, small classifiers trained on a model's hidden vectors to detect a concept. A probe shows that information is present, not that the model uses it, and it reads along a direction that we fitted. The tools here train nothing. They read the hidden vectors with the model's own output layer, the one it really uses to predict.
Section 23.1 ranked interpretability evidence on a ladder of four rungs, association, direct effect, causal effect, and mechanism. Both tools stand on the second rung. A direct effect is what one part of the model sends straight to the output while every other part is held fixed. We build the lens, a trained refinement of it called the tuned lens, and the exact algebra behind the split, and we run all of them on a real trained model in your browser. The section ends where direct effects stop working. A part can send almost nothing straight to the output and still be essential, and measuring that is the job of §23.4.
The model already contains a decoder
Start with how a Transformer is wired. Each token has one vector, the residual stream, that runs from the bottom of the model to the top. Every attention layer and every MLP reads the stream and adds its output back into it, and nothing is ever overwritten (§5.1). So the vector at the top is the embedding plus the sum of everything every layer wrote.
Now the notation. Token vectors are rows, so a weight matrix W∈Rdin×dout acts on a vector as xW. The model has L blocks of width d, each an attention layer followed by an MLP. Write xt(ℓ)∈R1×d for the stream at position t after ℓ blocks. So xt(0) is the token embedding plus the position embedding, and xt(L) is the final stream. Write xt(ℓ,mid) for the stream inside block ℓ, after its attention and before its MLP. (Section 5.1 counted sublayers. Here the superscript counts blocks.) Written out, the running sum is
Here h is the number of heads per layer. The term ct(ℓ,i)=Ht(ℓ,i)WO(ℓ,i) is what head i of layer ℓ writes. It is the head's output row Ht(ℓ,i)∈R1×dv times the head's own block WO(ℓ,i)∈Rdv×d of the output projection. We may split the projection head by head because concatenating the heads and projecting equals projecting each head and adding (§4.5). The vector bO(ℓ) is the attention layer's output bias, and ct(ℓ,mlp) is the MLP's write. We call each term a component and write a generic one as c.
The model reads the final stream exactly once. It applies the final LayerNorm and then the unembeddingWU∈Rd×V, where V is the vocabulary size. The unembedding turns a vector into one score per vocabulary token, called the token's logit,
In plain words, LayerNorm subtracts the vector's mean μ and divides by its spread s. Then it multiplies coordinate by coordinate by a learned gain γ and adds a learned shift β, both in R1×d (§5.2). Here 1∈Rd is the all-ones column, and ϵ is a small constant that keeps the division safe. The softmax of the logits is the model's next-token distribution, p(⋅∣t)=softmax(zt).
Column v of the unembedding, uv=(WU):,v, is token v's readout direction. Its dot product with the normalized stream is token v's logit. In a tied model, such as GPT-2 or the tiny GPT of Chapter 6, the unembedding is the transposed embedding table, WU=E⊤ (§6.2). An untied model learns WU as a matrix of its own. The circuit formulas of §6.8, and of this chapter, keep E and WU as separate symbols, so they hold in both cases.
Nothing stops us from applying this readout to the stream before the end. That is the whole idea of the lens.
In plain words, stop the forward pass early and ask the output layer what it would predict if the stream ended here.
There is a second way to see it. By the running sum, the model's real logits are LNf(xt(ℓ)+everything written after ℓ)WU. The logit lens replaces everything written after ℓ by zero. Whether that is a sensible replacement is an empirical question, and the answer depends on the model.
Three ways to read a lens
A lens gives a whole distribution over the vocabulary at every site. Three summaries of it are standard.
The top-1 token, the lens's best guess, argmaxvqℓ(v∣t).
The rank of the model's final top-1 token v⋆=argmaxvp(v∣t) under the lens. It is the number of tokens the lens scores strictly higher, #{v:qℓ(v∣t)>qℓ(v⋆∣t)}. Rank 0 means the lens already agrees with the final answer.
The KL divergence to the final distribution, DKL(p∥qℓ)=∑vp(v)log(p(v)/qℓ(v)). It measures how much worse qℓ predicts than p, on average over the model's own predictions. It is zero exactly when qℓ=p. With the natural log it is in nats. Dividing by ln2 gives bits, where one bit is the information in a fair coin flip.
The direction of the KL matters. Written this way it is an average over the model's own distribution p. So it charges the lens heavily wherever p has mass and qℓ does not. A qℓ trained on this direction learns to cover every token p finds likely, which is why §0.5 calls it mass-covering. This is the quantity the original post plotted, and the one the tuned lens below is trained to minimize.
Python · cell 1
Even this toy shows the lens reading something sensible at every site. It also shows two things we need later. The scale s changes from site to site, and the final answer is a sum of contributions.
Warm-up
Logit lens on a stack of states
Implement the logit lens for a whole stack of residual streams at once. The array states has shape (S, T, d). It holds the stream at S sites for T positions, and its last site is the final stream. You also get the final norm's gamma and beta, each of shape (d,), and the unembedding W_U of shape (d, V).
Write logit_lens(states, gamma, beta, W_U, eps=1e-5) that returns two arrays. The first, logp of shape (S, T, V), holds the lens log-probabilities. The second, rank of shape (S, T), holds the rank at each site of the model's final top-1 token, where rank 0 means the lens ranks it first. In the tests the streams grow a hundredfold in size and in mean with depth, as real ones do, so skipping the norm fails.
import numpy as np
def logit_lens(states, gamma, beta, W_U, eps=1e-5):
"""states: (S, T, d) residual streams at S sites, the last one final.
Returns logp (S, T, V), the lens log-probabilities, and rank (S, T), the rank of the
final top-1 token at each site (0 means it is the lens's top prediction there)."""
raise NotImplementedError
Tests, verified solutions, and AI help, open once you add your API key.
Core
KL to the final layer, stably
Score every lens reading against the model's final distribution. Write lens_kl(logits_lens, logits_final). The input logits_lens has shape (S, T, V) and holds the lens logits. The input logits_final has shape (T, V) and holds the model's own logits. Return (kl_nats, kl_bits), each of shape (S, T). They hold DKL(p∥q) for the final distribution p and each lens distribution q, in nats and in bits. Real lens logits can reach 103, and the function must stay finite there.
import numpy as np
def lens_kl(logits_lens, logits_final):
"""logits_lens: (S, T, V) lens logits; logits_final: (T, V) the model's logits.
Returns (kl_nats, kl_bits), each (S, T), the KL divergence D_KL(p || q) of each lens
distribution q from the final distribution p."""
raise NotImplementedError
Tests, verified solutions, and AI help, open once you add your API key.
What the lens shows in real models
The logit lens was introduced in a blog post on GPT-2 (nostalgebraist 2020). The post works mostly with the largest GPT-2, which has 1.5 billion parameters and 48 blocks, and its main plots read a passage of the GPT-3 paper's abstract.
You can run the same experiment on a real trained model here. The widget uses the 2-layer character-level GPT of Chapter 6, trained on Shakespeare's sonnets. It decodes five sites with the lens, the embedding and the stream after each attention layer and each MLP. Click any character of the prompt to read the prediction made after it.
Where inside a real trained model does its next-character prediction form?
Loading interactive figure…
Why does the bottom of a tied model predict its own input? In a tied model, token w's readout direction is its own embedding ew, row w of the embedding table. At the embedding site the stream is the input token's embedding ev plus a position vector. So the lens logit of token w is roughly ev⋅ew. By the Cauchy-Schwarz inequality, ev⋅ew≤∥ev∥∥ew∥, and the self term ev⋅ev=∥ev∥2 meets this bound. So the dot product is usually largest at w=v. This is a fact about tied geometry, not evidence that the model is "thinking about" its input.
How much of the convergence is evidence that the model refines one guess? Less than it looks. The stream is a running sum, so a late site shares almost all of its terms with the final stream. Unless the last few blocks write something large, its lens reading will be close to the final one, whatever the network computes. The next cell checks this on a network that was never trained.
Python · cell 2
The random network's lens converges smoothly too. Its KL to the final distribution falls from 7.5 bits to 0, and its top-1 agreement with the final answer climbs from 0.09 to 1. Convergence is what the arithmetic of a sum gives you for free. Two things do carry information. One is what the lens says along the way, such as plausible continuations in the middle layers rather than noise. The other is how much earlier it matches the final answer than an untrained baseline like this one does. In practice, never report "the lens converges" as a finding. An interviewer will ask what an untrained model shows.
Where the logit lens breaks
The tuned-lens paper ran the logit lens on many more models and found it unreliable (Belrose et al. 2023). On GPT-Neo-2.7B, reading an excerpt from the abstract of the Transformer paper, the lens gives no interpretable predictions before layer 21 of 32. On BLOOM and OPT-125M, the lens's top-1 token is often the input token itself, rather than any plausible continuation, in more than half the layers.
Even where the lens looks sensible, it is biased, in a sense the paper made precise. Average the model's final distribution over every position of every sequence in a dataset, to get a marginal distribution pˉ(v). Do the same for the lens at layer ℓ, to get qˉℓ(v). The bias is DKL(pˉ∥qˉℓ). An unbiased lens puts, on average, as much probability on each token as the model does.
Three mechanisms explain most of the failures. Each is worth knowing, because each also breaks other tools that read the stream.
Later writes are not zero on average. The lens replaces everything written after layer ℓ by zero. Suppose later blocks add a large vector that is roughly the same for every input, a constant bias. Then zero is a strange replacement, and the lens reads a stream the final norm never sees in practice. The cheapest fix replaces zero by a learned constant bℓ, which the paper calls the debiased logit lens.
Representational drift. The statistics of the stream change with depth. In Pythia 12B, layer 4 introduces two outlier dimensions that dominate the covariance of the hidden states. With those two removed, the covariances of two layers grow steadily less similar the farther apart the layers are (Belrose et al. 2023). A readout trained for the last layer then misreads the early ones. This is the rotation worry from above, in a milder form.
Massive activations. A handful of activations in large language models are orders of magnitude larger than the rest (Sun et al. 2024). In LLaMA2-7B they live in two fixed coordinates (1415 and 2533), and they appear only at the starting token and at the first period or newline. They appear abruptly at layer 2 and stay nearly constant until layer 30. In LLaMA2-70B they exceed 15,000 in absolute value. They act as fixed bias terms. They also draw attention to their tokens, which ties them to attention sinks, the low-information tokens where heads park attention they do not want to place anywhere (§4.4).
Here is what one massive value does to the lens. A value a in coordinate k makes s≈∣a∣/d, so the normalized stream is nearly d times a one-hot vector at k. The lens then returns whatever tokens row k of WU favors, times the gain γk and the sign of a, regardless of context. So treat lens readings at the first position and at delimiters with suspicion. Massive activations are not the outlier features that make 8-bit activation quantization hard. Those are a few channels that are large at almost every token, while a massive activation is one value at one token. Sun et al. (2024) checked that none of the outlier-feature channels they found coincide with the massive-activation coordinates. Section 20.2 meets both kinds of extreme value from the quantization side, where either one sets the step of a per-tensor scale.
One more limit is structural. The lens sees only what WU can read. A feature the model computes for internal use, such as that the subject is plural or that a quote is still open, can sit in directions the unembedding ignores. No lens reading will ever show it. A lens is a probe whose readout was trained for the last layer only.
The tuned lens
If each layer writes in a slightly different basis, learn a translation for each layer. The tuned lens (Belrose et al. 2023) adds one affine map per layer, from that layer's stream to the space the final norm expects. It is trained so that the decoded distribution matches the model's own.
In plain words, teach each layer's stream to speak the last layer's language, using the model's own output as the answer key.
Three design choices deserve a reason.
The loss is a distillation loss. Expand the KL, DKL(p∥q)=H(p,q)−H(p). The entropy H(p) does not depend on the translator. So minimizing the KL is minimizing the cross-entropy of the lens against the soft labels p. That is knowledge distillation, training a student to match a teacher's full output distribution (§20.4). Here the teacher is the full model, and the student is "layer ℓ plus a translator". Matching the model's outputs, rather than the true next tokens, matters. A probe trained on true labels is rewarded for extracting anything predictive, even information the model never uses, which was the worry behind control tasks in §23.2. A lens trained to match p has no reason to know more than the model.
Identity initialization. Starting at the logit lens means training learns only the departure from it. In layers that already share the final basis, the translator stays near the identity. So a large learned change is itself a measurement of drift.
A translator, not a new unembedding. A fresh readout per layer would have V×d parameters, 38,597,376 for GPT-2 small. A translator has d2+d=590,592. The paper reports that training new unembeddings needed more steps and larger batches and often reached a worse perplexity, the exponential of the average cross-entropy, where lower is better.
The results were clear. The tuned lens had much lower perplexity than the logit lens across the board, including on BLOOM and GPT-Neo, where the logit lens fails. It also had much lower bias on GPT-Neo-2.7B, the model for which the paper plots bias. The tuned lens also relies on the same features the model does, a check described in the box below.
The widget isolates the three failure mechanisms. It builds a stream as a sum, so that each block adds a share of the answer. Then it distorts the earlier sites in three ways. A basis rotation and a bias drift both grow linearly from nothing at the top to their full size at the embedding, and sites 2 to 7 carry one huge constant coordinate. It compares the logit lens with an affine translator fit by least squares.
Why does the logit lens fail when layers drift, and how does an affine translator fix it?
A synthetic 8-block stream, each block adding an eighth of the answer. The sliders distort the earlier sites.
Loading interactive figure…
All three sliders start at zero, where the two curves nearly coincide. The fitted lens is a little worse there, because least squares matches the final stream, not the final distribution. Turn the rotation up first. The logit-lens curve rises above the tuned one, most over the first half of the sites, which are rotated the most. The bias drift and the outlier coordinate each break it at every site they touch. The tuned-lens curve does not move at all. The translator undoes all three distortions, because each one is affine. What it cannot undo is the part of the answer that later blocks have not written yet, which is why the tuned-lens curve still falls with depth. That remaining curve is the honest picture of when information arrives.
Research
A least-squares tuned lens
The real tuned lens is trained on the KL. A closed-form stand-in fits each translator by least squares, from layer-ℓ states to final states. Din et al. (2023) proposed such maps, without the bias, as shortcuts through a model, and Ghandeharioun et al. (2024) used an affine map of this kind as a baseline. Build it in two steps.
First, write fit_translator(H_l, H_L, ridge). It returns (Tm, b) that minimize ∥HℓT+1b−HL∥F2+λ∥T∥F2, with λ = ridge. Here ∥⋅∥F is the Frobenius norm, the square root of the sum of squared entries, and the intercept b is not penalized.
Second, write tuned_lens_report(states, gamma, beta, W_U, ridge, n_train). It fits one translator per site on rows [:n_train]. It returns (kl_logit, kl_tuned), the mean KL to the final distribution in bits on the held-out rows [n_train:], one number per site. The setup gives you lens_logp(X, gamma, beta, W_U) and mean_kl_bits(logp, logq). In the tests, the earlier sites of the stack are rotated, shifted, and noisy.
import numpy as np
def fit_translator(H_l, H_L, ridge=0.0):
"""Affine least squares from layer-l states H_l (n, d) to final states H_L (n, d).
Returns (Tm, b), Tm (d, d) and b (d,), minimizing ||H_l Tm + b - H_L||^2 + ridge ||Tm||^2
(the intercept b is not penalized)."""
raise NotImplementedError
Tests, verified solutions, and AI help, open once you add your API key.
Here is the real way to train one, in PyTorch, on a frozen toy network. The model's blocks write large constants, which is the bias problem. Each block also adds information from a source the earlier states never saw, a stand-in for attention bringing in other tokens. The code records the stream entering each block, which is what a forward hook does in a real model (§0.9). Each translator starts at the identity and is trained by Adam on DKL(p∥q). In PyTorch's argument order that is F.kl_div(log_q, log_p, log_target=True).
🔥 In PyTorchfor a machine with PyTorch
The logit lens is 2 to 8 bits off at the first five sites. It is not even monotone, because each block's constant write pushes the stream somewhere the final norm never sees. The tuned lens falls steadily with depth. Its remaining KL is the information that later blocks bring in, which no translator of an earlier state can recover.
What the tuned lens is used for
A prediction trajectory is a compact fingerprint of how the model processed an input. That makes it useful for spotting inputs that the model processes abnormally.
Lenses also power decoding methods. Decoding by contrasting layers, or DoLa (Chuang et al. 2024), compares an early layer's lens distribution with the final one and favors the tokens whose log-probability grew the most between them. That improved factuality without any training, as §19.8 describes.
Any lens is also an early exit in waiting, a way to stop the forward pass before the last layer. If layer 20 of 32 already predicts the final token with confidence, generation could stop there and skip the last 12 blocks, more than a third of the compute. Confident adaptive language modeling, CALM (Schuster et al. 2022), trains the model so that every layer can be decoded and exits when a confidence measure passes a threshold. A lens gives a similar signal for a frozen model.
The most general view is Patchscopes (Ghandeharioun et al. 2024). Take the hidden state at layer ℓ and position t of a source prompt, and optionally transform it. Patch it into a forward pass on a target prompt, at some layer and position, and read what the model generates. The logit lens and the tuned lens are the special case in which the state is patched into the last layer, so the target prompt never matters. Patching into a middle layer instead lets the model's own later layers do the decoding. The price is one forward pass per reading and a reliance on the model's ability to put what it knows into words.
Folding the final LayerNorm into the unembedding
Now turn from the trajectory to the final logits, and split them exactly into contributions. The obstacle is the final LayerNorm, which sits between the stream and the logits. Look at its four steps. Subtracting the mean is linear. Multiplying by the gain is linear. Adding the shift adds a constant. Only the division by s depends on the whole vector in a nonlinear way. So everything except that one number can be moved into the weights, which is called folding the norm into the unembedding.
The algebra uses one matrix, the centering matrixC=I−d111⊤∈Rd×d. For a row x, xC=x−(d1x1)1⊤=x−μ1⊤, so multiplying by C subtracts the mean. For example, (1,2,3)C=(−1,0,1). The matrix is symmetric. It is idempotent, C2=C, since centering twice changes nothing. And 1⊤C=0⊤. It is the projection onto the vectors with zero mean (§0.10).
Proof. Write the gain as a matrix, γ⊙y=ydiag(γ) for a row y. Then
using x−μ1⊤=xC and the associativity of matrix products. The columns of WU′ sum to zero because 1⊤WU′=(1⊤C)diag(γ)WU=0⊤. So adding c1⊤ to x adds c1⊤WU′=0⊤. For the second claim, let z=s1xWU′+bU. The vocabulary-centered logits are z(I−V111⊤)=z−zˉ1⊤ with zˉ=V1z1, the same constant subtracted from every logit. Softmax is unchanged by adding a constant to all of its inputs (§0.4 and §4.3 use the same fact for stability), and a difference za−zb loses the constant on both sides. ∎
In plain words, the gain and the mean subtraction are linear, so they can live in the weights. The shift becomes a fixed bias on the logits. Only the division by the one number s stays outside.
RMSNorm, the norm in LLaMA and most models since (§5.2), has no mean subtraction and usually no shift. It folds with WU′=diag(γ)WU, and s is replaced by the root mean square d1∑jxj2+ϵ.
The cell checks the theorem on random numbers, with streams whose means are large and different.
Python · cell 3
Folding is exact to rounding error. Centering over the vocabulary moves raw logits by more than a unit, while it leaves log-probabilities and differences untouched, and the centered logits average to zero. That last property is why centered logits are easier to read. A component that adds the same amount to every logit no longer looks important.
Interview
Fold LayerNorm into the unembedding
Write fold_ln(gamma, beta, W_U) that returns (W_U_f, b_U). The folded unembedding W_U_f must be centered over d, so each column sums to 0, and over the vocabulary, so each row sums to 0. The folded bias b_U must also be centered over the vocabulary. Then write folded_logits(x, W_U_f, b_U, eps=1e-5). It takes raw, uncentered streams x of shape (n, d) and returns the folded logits. The probabilities must match those of LN_f(x) @ W_U to 10−10. Interviewers ask this because it tests whether you know which parts of a normalization are linear.
import numpy as np
def fold_ln(gamma, beta, W_U):
"""gamma, beta: (d,); W_U: (d, V). Returns (W_U_f, b_U) with W_U_f (d, V) and b_U (V,),
the final LayerNorm folded into the unembedding, centered over d and over the vocabulary."""
raise NotImplementedError
def folded_logits(x, W_U_f, b_U, eps=1e-5):
Tests, verified solutions, and AI help, open once you add your API key.
Splitting a logit difference exactly
Which quantity should we split? A single logit za is a poor target. Adding the same number to every logit changes nothing the model does, so the size of za alone means little (vocabulary centering exploits exactly this). A probability is a poor target too. It depends nonlinearly on every logit, and near 0 or 1 it saturates, so large changes in the stream barely move it. The standard choice is a logit difference between a correct token a and a plausible wrong token b, called the foil,
Δt=zt,a−zt,b=logp(b∣t)p(a∣t).
In plain words,Δt is the log-odds of the right answer over one specific wrong answer. For example, p(a)=0.6 and p(b)=0.2 give Δ=ln3≈1.10.
It has four good properties. It ignores a constant added to all logits. It does not saturate. It has a meaning in probability terms. And, as the theorem below shows, it is linear in the stream once s is fixed. It also asks a sharp question ("why Paris rather than Rome?") instead of a diffuse one. The study of how GPT-2 completes "When Mary and John went to the store, John gave a drink to" with "Mary" uses the difference between the two names for exactly these reasons (§23.4). In the stream, the direction that raises Δt is ua−ub, scaled coordinate by coordinate by the gain and then centered, because the norm removes the mean.
Proof. By the folding theorem, zt=s1xt(L)WU′+bU, so Δt=zt,a−zt,b=s1xt(L)(ua′−ub′)+(bU,a−bU,b). Substitute the sum for xt(L) and distribute the product over it. For one component, c(ua′−ub′)=cCdiag(γ)(ua−ub)=(c−cˉ1⊤)⋅w. The bias term is bU,a−bU,b=β⋅(ua−ub). That the attention layer's output splits into the head writes plus bO(ℓ) is the block identity of §4.5. ∎
In plain words, center each write, shrink it by the one shared s, and measure how far it points from b toward a. The pieces add up to the logit difference to the last digit.
Section 5.1 split a single logit the same way, in Exercise 5.1b. This version adds three things that matter in practice. It splits attention by head, it targets a difference, and it accounts for the biases.
The by-hand example splits the same way. Its final stream (3,−1,0,0) has s=23. With WU=I and unit gain, w=(1,−1,0,0).
Python · cell 4
The head's direct effect on Δ is exactly zero, because it raised Paris and Rome by the same amount (by 1 each, as the last printed line shows). This is what DLA is for. It turns "the model said Paris" into a ledger with one line per component.
A model whose answer we know
Now take a model with real attention and a known answer. The cell builds a compact version of the hand-wired model that §23.4 studies in depth. It mimics how GPT-2 finishes "When Mary and John went to the store, John gave a drink to". The right answer there is the name that appeared only once, the indirect object of "gave". Our version is a three-layer, attention-only Transformer with one head per layer. Its sequences look like BOS A B f f B f END. Here A and B are names, f is a filler token, and BOS and END mark the start and the end. The correct prediction at END is A, the name that appears once.
Each head does one job, and together they form a circuit, a set of components that computes one behavior.
Head 1, a duplicate-token head, writes into a DUP slot at the second B that B has been seen before.
Head 2, an S-inhibition head (S for subject, the repeated name), moves that flag to END and pushes B's logit down a little directly.
Head 3, a name mover, attends at END to names that are not flagged and copies the one it finds into the output slot that the unembedding reads.
This version has five names, sharp attention scores (a scale of 20), and a final LayerNorm, so that the scale s in the decomposition does real work. Section 23.4 rebuilds the model with ten names, softer scores, and no final norm, so that its patching numbers can be checked by hand.
Python · cell 5
Python · cell 6
Python · cell 7
The widget below runs the same split on the real tiny GPT, at the last position of the prompt. Its dashed boxes are total effects, the drop in Δ when a part's output is set to zero at every position and the model is rerun.
Can a part that writes almost nothing into the answer still be essential?
Loading interactive figure…
In practice, DLA is how you find the last step of a computation, the components that write the answer into the logits. An interviewer will ask the follow-up at once. What about the components that only tell the last step what to do? DLA cannot see them, and the rest of this section and all of §23.4 are about that gap.
Interview
Per-head direct logit attribution for a logit difference
Compute the per-component ledger for one position of a pre-LN Transformer with L layers of h heads. You are given these arrays.
head_out of shape (L, h, d_v), the head outputs before WO.
W_O of shape (L, h * d_v, d), the output projections. Rows i * d_v to (i + 1) * d_v belong to head i.
mlp_out of shape (L, d), the MLP writes, and b_O of shape (L, d), the attention output biases.
embed of shape (d,), the token plus position embedding.
The final norm's gamma and beta, the unembedding W_U, and two token ids a and b.
Write dla_logit_diff(...) that returns a dict with five entries. The entry 'heads' has shape (L, h), the entries 'mlp' and 'b_O' have shape (L,), and 'embed' and 'beta' are single numbers. They are the direct contributions to za−zb, and they must add up to it exactly. This is the computation behind every "which heads matter" bar chart, and the tests check the four mistakes people make with it.
import numpy as np
def dla_logit_diff(head_out, W_O, mlp_out, b_O, embed, gamma, beta, W_U, a, b, eps=1e-5):
"""One position of a pre-LN Transformer with L layers of h heads.
head_out: (L, h, d_v) head outputs before W_O; W_O: (L, h * d_v, d); mlp_out: (L, d);
b_O: (L, d); embed: (d,), token plus position embedding; gamma, beta: (d,); W_U: (d, V).
Returns a dict with 'heads' (L, h), 'mlp' (L,), 'b_O' (L,), 'embed' and 'beta' (floats),
the direct contributions to z_a - z_b, which must add up to it exactly."""
Tests, verified solutions, and AI help, open once you add your API key.
What freezing the scale hides
The decomposition is an identity, so where is the approximation? It is in the interpretation. Calling a term "the effect of c" means imagining c gone with everything else held fixed, including s. But s is computed from the whole stream, so deleting c changes it, and with it the size of every other term. This happens even if no later layer reacts at all, and it can be large. Two numbers decide how large.
∂ Where the square root and the first-order term come from
With ϵ=0, s2=d1∥x~∥2. After deleting c the centered stream is x~−c~, so
The other components contribute s1(x~−c~)⋅w=r−Dc before the deletion and s′1(x~−c~)⋅w=(r−Dc)s/s′ after it, which is the exact formula. For the first-order form, expand (1+u)−1/2=1−21u+O(u2) with u=−2ρc+κc. Since κc is second order in the size of c while ρc is first order, s/s′=1+ρc+O(∥c~∥2). The same result comes from the gradient of 1/s, which is −x~/(ds3) (using ∂s/∂xj=(xj−μ)/(ds) from §5.2), dotted with the change −c.
In plain words, a component that points along the stream's own direction props up the scale. Its alignment ρ is the share of the stream's squared length that it accounts for along that direction, and the alignments of all the components add up to 1. Delete it and s shrinks, every surviving term grows by about the fraction ρ, and the logit difference can rise or fall by far more than the component's direct effect.
Go back to the by-hand example. The head's direct effect was exactly 0, yet deleting it, with the MLP's write held fixed, moves Δ.
Python · cell 8
In a real model the MLP's write would change too, since the MLP reads the stream the head wrote into. The true total effect adds that change on top.
At realistic sizes the effect is usually smaller, but not always. The next cell builds a stream of six random writes in d=64, one of them partly aligned with the largest. It compares each write's direct effect with the exact change from deleting it.
Python · cell 9
Small writes that are nearly orthogonal to the stream (parts 3 to 5) behave as DLA says, to within about a percent. Part 1 has a direct effect of +0.17. But it is aligned with the stream (ρ=0.37), so deleting it lowers Δ by 2.92, seventeen times more, and the first-order correction recovers most of that. Part 0, the largest write, has a direct effect of −4.19, yet deleting it raises Δ by only 3.06.
The lesson for practice is to report a direct effect together with the component's ρ and κ, and to measure total effects separately by intervention. A study of self-repair in Chinchilla 7B did both. It computed direct effects through the unembedding with the final RMSNorm's scale held fixed, and compared them with ablations (McGrath et al. 2023).
Reading weights in vocabulary space
The same readout works on weights as well as activations, and then it needs no input at all. Section 5.3 described an MLP as a key-value memory. Let W2∈Rdff×d be its output matrix. Row i is the value vectorvi of hidden neuron i, and the MLP's write is ∑imivi, where mi is neuron i's activation. Each term mivi is called a sub-update (Geva et al. 2022). Its direct effect on the logits is smiviWU′. So the tokens a neuron promotes whenever it fires can be read off once, from viWU, without running the model.
The cell plants three neurons that each promote one group of tokens, and adds weak background activity. Then it compares the projection of the whole update with the projections of its pieces.
Python · cell 10
The whole update's top 10 tokens are nine from one group and one from another. They hide entirely that the layer also promoted the third group. Each of the three planted sub-updates is pure. Real MLPs have thousands of neurons and far messier value vectors, but the arithmetic is the same.
Every weight that writes into the stream can be read this way. A head's OV circuitWOV=WVWO is the product that decides what the head writes once it has chosen where to look (§4.2). It gives the V×V matrix EWOVWU, whose entry (j,k) says how much the head raises the logit of token k when it attends to token j. If the diagonal dominates, the head copies the token it attends to. The copying score of §6.8 measures this from the matrix's eigenvalues, as the sum of their real parts divided by the sum of their absolute values. The product EWU alone is the direct path of §5.1, the scores the model would give with no layers at all, which in trained models looks like a table of bigram statistics. Dar et al. (2022) pushed this furthest. They projected every parameter of a Transformer into the vocabulary, including the attention scoring weights, and also used it to align models that share a vocabulary.
Three caveats apply to all of it. A weight projection shows what a component can write, not what it writes on a given input, where the activation mi may be zero or negative (a GELU outputs small negative values). It ignores the norm's scale and every indirect path. And the columns of WU are not orthogonal, so a vector that points at one token also raises similar tokens, which can make a projection look more meaningful than it is.
Doing it at scale
On a real checkpoint you would not write any of this by hand. Here is the same pipeline with the TransformerLens library, shown but not run, since it downloads GPT-2.
The code avoids one trap. The call accumulated_resid(apply_ln=True) divides every site by the final stream's cached scale. That is right for attribution and wrong for a lens, because an early site's own scale can be several times smaller. Probabilities and KL come out wrong. Even ranks can change, because the folded bias bU is added without being rescaled. So for a true logit lens, apply model.ln_final to each site afresh, as the code does.
Choosing a tool
Method
What it measures
Training
Cost per reading
Main failure
Logit lens
What a site would predict if every later write were zero
none
one norm and one d×V product
biased, misreads drifting or outlier-heavy layers, blind to features WU ignores
Tuned lens
Best affine reading of a site in the final basis
one d×d translator per layer, by KL
the same plus a d×d product
misses drift that is not affine, needs a training run for each model
Token-identity Patchscope
What the model itself verbalizes from a patched state
none
a forward pass per reading
depends on the prompt and on the model's ability to verbalize
Direct logit attribution
Each component's direct contribution to za−zb
none
one cached forward pass for all components
misses indirect effects, self-repair, and the norm effect of removal
All but the last read what the stream or the weights say to the output, without asking what would change if something were different. Use them to find candidates cheaply, then confirm with interventions.
Read more
nostalgebraist (2020). interpreting GPT: the logit lens. The original post, with GPT-2's prediction trajectories, ranks, KL plots, and the puzzle of the vanishing input.
Dar et al. (2022). Analyzing Transformers in Embedding Space. Interpreting every parameter of a Transformer by projecting it into the vocabulary, without a forward pass.
🎬 Neural Mechanics Week 1: LLM Foundations and Logit Lens with David Bau · NDIF TeamA leading interpretability researcher builds up the residual stream and the logit lens from the model's own readout, the core of this section.🎬 UUtah CS 6966 Interpretability of LLMs | Spring 2026 | Direct logit attribution & Logit lens · UofU Data ScienceA university lecture that covers direct logit attribution and the logit lens together, a good second pass over this section's two main tools.