Opening case
The answer that appeared between two additions
A tiny transformer receives five tokens. Its task is to repeat the first token when it reaches a fixed query marker at the end. For one test sequence, the correct token is 7. A readout from the query position gives token 0 immediately after embedding, token 7 with probability 0.86 after attention, and token 7 with probability almost 1 after the feed-forward sublayer.
The tempting account is simple. Attention found the answer; the feed-forward network merely polished it. Then we intervene. Zero the attention output and the model fails. Zero the feed-forward output and it still predicts 7, but less confidently. Corrupt the first token, then copy only the clean attention write into the corrupted run, and the correct answer returns.
That sounds decisive until a second possibility appears. Another block can write strongly towards 7 in attention and then erase that direction in its feed-forward sublayer. The final output may be identical to a block that never wrote 7 at all. A sensor at the end sees the same result. A sensor between sublayers tells two different causal stories.
The hidden assumption is that each module owns a representation and hands it to the next module. In a residual architecture, modules do not take turns owning the state. They read from and write into a shared stream whose later consumers can reinterpret every earlier contribution. The block becomes intelligible only when we separate the continuity of the stream from the updates written into it.
The representation nobody owns
The residual stream is not another learned module. It is the path along which the current token states persist while sublayers propose changes.
Begin with a river, not a pipeline
A pipeline suggests replacement. Stage one emits a representation; stage two consumes it and emits another. A transformer block is easier to understand as a river. The token embeddings enter as the upstream current. Attention adds a tributary whose content depends on relations among token positions. The feed-forward sublayer adds another tributary whose content depends on the current state at each position. The MLP is position-wise, but that state already contains context routed by attention. Empirical analysis of trained transformers has shown feed-forward updates promoting output-token distributions, which is one reason their residual writes deserve separate inspection.[7] The water downstream contains all three contributions, though later currents may mix or cancel them.
Computation as a dye stream
One state, three causal views
“Shared state” does not mean that every component sees the same object in the same way. A block exposes three related but distinct views. The read view is the normalised state presented to a sublayer. The write view is the vector that sublayer returns in residual-stream coordinates. The use view is the effect of that changed state on the frozen computation that follows. Treating these as one view is the source of many mistaken explanations.
A direction can persist on the skip path while being invisible to the next normalised read. A component can make a large write that lands in a direction to which the suffix is insensitive. A very small write can matter when the current state sits close to a decision boundary. Conversely, a write that looks decisive under the final unembedding can be cancelled before the model acts on it. Scale, readability and leverage are therefore different properties.
The block’s causal contract can be stated as three questions: what state did the component condition on, what delta did it add, and which downstream operation changed because of that delta? The residual-stream framework makes the first two questions easy to instrument.[6] The third still requires an intervention. This is why an activation trace is evidence about computation, but not yet a complete explanation of it.
Replace both learned sublayers with functions that always output zero. Keep the skip paths. What leaves the block? Exactly what entered. Now remove the skip paths and keep the zero sublayers. Nothing useful survives. One variable changed: the existence of the identity route.
The result reveals the hidden contract. With a skip, the learned function can ask, “What correction should I add?” Without it, the function must ask, “What complete state should I rebuild?” Residual learning was introduced in deep vision networks to make such residual functions easier to optimise than unreferenced mappings.[2] The transformer inherits the same structural advantage, then uses it twice per block.
The smallest honest equation
For a common pre-normalised decoder block, let rl be the matrix of token states entering layer l. It has one row per token position and one column per residual dimension. Let N be a normaliser, A the attention sublayer and M the feed-forward sublayer. The block computes:
rl+1 = ul + Ml(Nl,m(ul)) Here, ul is the state after attention. Each sublayer reads a normalised view and writes an update in the residual stream’s original width.
Across a stack, the final pre-output state can be written as the embedding contribution plus every attention and feed-forward write observed during that forward pass:
This distinction matters. The sum tells us where to place readouts and interventions. It does not tell us that a capability belongs to one addend. A late MLP may respond to a feature routed by early attention. An attention head may suppress a direction written by another head. A final normaliser can rescale the whole mixture before unembedding. Additivity of state is not additivity of responsibility.
Addition commutes; conditional writing does not
Vector addition itself is commutative. If two already-computed writes are a and m, then r + a + m equals r + m + a. It is tempting to conclude that the attention and feed-forward branches could be reordered without changing the block. The hidden assumption is that the writes were computed independently of the evolving state.
They are not. In the usual sequential block, attention writes a = A(N(r)). The feed-forward network then writes m = M(N(r + a)). If the order is reversed, the MLP receives r, while attention receives a state already altered by the MLP. Both writes can change. Parallel block variants deliberately change this dependency structure; they are more than another drawing of the same sum.
Ignore normalisation and take a one-number state r = 1. Let attention write A(x) = x², and let the MLP write M(x) = x + 1. Attention first gives 1 + 1 = 2; the MLP then writes 3, so the block ends at 5. Reverse the order: the MLP first writes 2, producing 3; attention then writes 9, so the block ends at 12. The additions did not become non-commutative. The functions generated different addends because the read state changed.
This tiny example removes one common ambiguity. A residual decomposition is a record of the realised forward pass, not a bag of context-free causes that can be rearranged after the fact.
Anatomical cutaway of a pre-LN block
r_l, attn_write, u_l, mlp_write and r_l+1. Illustrative architecture view, based on the pre-LN equations.Suppose one token begins at r0 = [2, 0]. An attention head writes a = [0, 1], producing [2, 1]. The MLP then writes m = [-1, 2], producing [1, 3]. An unembedding compares two token directions: token A reads the first coordinate; token B reads the second.
At the input, A wins 2 to 0. After attention, A still wins 2 to 1. After the MLP, B wins 3 to 1. Nothing in the MLP had to reproduce the first two coordinates. It only had to add the update that changed the decision. The example assumes a linear readout and no final normalisation. Add a nonlinear normaliser and the exact logits change, though the additive state path remains.
One token’s path through residual space
flowchart TD A["The answer that appeared<br/>between two additions"] --> B["The representation nobody owns"] B --> C["Normalisation decides what a<br/>write can see"] C --> D["A visible write is not a<br/>causal contribution"] D -. "re-test" .-> A classDef first fill:#0a1424,stroke:#5B5CF6,stroke-width:2px,color:#e6f3ff classDef step fill:#0a1424,stroke:#3f7da0,stroke-width:1px,color:#e6f3ff class A first class B,C,D step
Normalisation decides what a write can see
Normalisation is not decorative housekeeping. It changes the coordinates presented to a sublayer and the route through which gradients cross the block.
Change one variable: move the gate
The original Transformer placed layer normalisation after each residual addition: LayerNorm(x + Sublayer(x)).[1] Many later language-model architectures moved normalisation before the sublayer. The forward functions still contain attention, an MLP, addition and normalisation, yet the optimisation problem changes because the identity route is no longer passed through a normaliser at every step.
A factory conveyor carries a part past two machines. A measuring gate can sit before each machine or after each machine-plus-bypass merge. The same machines and bypasses remain. In the first layout, each machine receives a standardised view while the original part continues along a clean route. In the second, every merged part is standardised before it continues.
Would the factory learn the same adjustments under noisy calibration? Not necessarily. Moving the gate changes which transformations lie on the direct path from early state to late loss. This is the architectural difference between common pre-LN and post-LN blocks.
What layer normalisation actually removes
For one token vector x with d features, LayerNorm subtracts the feature mean, divides by the feature standard deviation, then applies learned gain and bias. Ba, Kiros and Hinton introduced it as a per-case alternative to batch-dependent normalisation, with the same computation at training and test time.[3]
Take x = [3, 1, -1]. Its mean is 1, so the centred vector is [2, 0, -2]. Now add 5 to every coordinate, giving [8, 6, 4]. The centred vector is still [2, 0, -2]. Ignoring learned gain, bias and the small numerical constant, LayerNorm presents the same direction to the next sublayer.
RMSNorm removes the mean subtraction and divides by root mean square instead. It preserves invariance to positive global rescaling but not to uniform shifts, reducing the operation while changing the symmetry it enforces.[5] The choice therefore says which global changes a sublayer should treat as irrelevant.
Normalisation creates equivalence classes
Ignoring the small stabilising constant, LayerNorm maps every vector of the form αx + c1, for positive α, to the same centred and standardised pattern before learned gain and bias. The sublayer cannot distinguish a uniform shift c1, and it treats a positive global rescaling as the same direction. RMSNorm identifies positive rescalings but keeps sensitivity to uniform shifts. These are not philosophical claims about what the model “knows”. They are exact or approximate symmetries of the read operation.
Pre-LN then creates a useful separation. The raw residual state, including components that the normaliser suppresses, continues along the identity route. The branch reads only the equivalence class selected by the normaliser. A fact can therefore be present in the stream yet unavailable to a particular branch under its read map. Whether it later affects output depends on later normalisers, projections and writes. Presence in state and addressability by a sublayer are not the same condition.
The gradient path exposes the same architectural distinction. For a simplified pre-LN update y = x + F(N(x)), the Jacobian is I + JFJN. It contains an explicit identity term. For post-LN, y = N(x + F(x)), the Jacobian is JN(I + JF); the direct route is filtered by the normaliser’s Jacobian. Xiong and colleagues used this difference to analyse why pre-LN and post-LN transformers exhibit different gradient behaviour at initialisation.[4] The identity term does not guarantee good optimisation. It explains which route exists before data and training reshape it.
Pre-LN and post-LN put different operators on the identity route
A small experiment makes the path visible
To expose the mechanism without claiming a new benchmark, we initialised matched 18-block synthetic networks. Each block contained two small GELU feed-forward operators standing in for attention and the MLP. One stack used pre-LN; the other used post-LN. We measured the median parameter-gradient norm at each depth across 30 random seeds under the same mean-squared-error objective.
Normalisation placement changes the gradient profile
The ingredients and equations are established. Their best placement remains an active design problem. Peri-LN adds output normalisation around sublayers and reported lower variance growth and improved stability in its tested pre-training settings.[11] Other work has replaced statistical normalisation with bounded pointwise functions such as Dynamic Tanh, and a later Derf variant reported stronger results in its tested domains.[12][13]
The safe conclusion is narrower than “normalisation is essential”. Deep blocks need some workable control of scale, sensitivity and signal propagation. LayerNorm, RMSNorm and newer alternatives implement different versions of that control.
Do not infer semantic importance from normalised magnitude alone. LayerNorm couples coordinates through a shared mean and variance; RMSNorm couples them through shared root mean square. A large raw write can be damped, and a small directional write can matter if it aligns with a sensitive downstream readout. Norms describe scale, not meaning.
A visible write is not a causal contribution
Residual additivity makes intermediate inspection convenient. It also creates a trap: direct readability can be mistaken for responsibility.
The logit lens is a question, not an answer
A decoder-only language model usually applies a final normaliser and an unembedding matrix to the last residual state. If we reuse that output machinery at an earlier layer, we obtain an intermediate vocabulary distribution. This “logit lens” asks which tokens are readable from the current state under the final decoder. A tuned lens learns a separate affine translator at each layer and has been shown to improve reliability over the raw logit lens in tested models.[8]
The readout is useful because it turns an opaque vector into a prediction trajectory. Its assumption is strong: the intermediate representation is sufficiently aligned with the final decoder. Earlier layers may use a different basis or encode information for later nonlinear processing. Final normalisation also means that raw component projections do not always compose as a simple sum unless the normalisation effect is handled explicitly.
System A begins with zero support for token P. Attention writes +4 units towards P. The MLP writes -1, leaving +3. System B has attention write 0 and MLP write +3. Both finish at +3, so their final logits match.
Now ablate attention. System A falls to -1; System B stays at +3. The output alone underdetermines the route. Direct attribution to the final token direction reveals that A’s attention promoted P, but only an intervention reveals that downstream computation depended on that write in this context.
Promotion, erasure and the misleading snapshot
Direct projection is local accounting
Let dp,q be the difference between the unembedding directions for tokens p and q. For a component write w, the quantity dp,qTw is its direct contribution to the p-versus-q logit difference under a linear readout. This is a clean accounting identity when no final normaliser intervenes. With a final LayerNorm or RMSNorm, the exact output change depends on the whole residual state, so analysts either apply the full readout to counterfactual states or use a stated local approximation.
Even when computed exactly, direct projection answers only what the write points towards at that site. It does not include how the write changes a later attention pattern, opens an MLP feature, alters normalisation statistics or triggers a compensating write. For that reason, three numbers must remain separate:
| Quantity | Question answered | What it can miss |
|---|---|---|
| Direct write | Which output direction does this delta favour immediately? | Later routing, nonlinear use, erasure and final normalisation. |
| Realised effect | How does the chosen metric change when the branch is replaced? | Redundancy, off-manifold damage and dependence on the replacement rule. |
| Patch recovery | Can a clean internal difference restore behaviour in a matched corrupted run? | Collateral information carried by a broad patch and incompatibility with the receiving state. |
No one scalar is “the contribution”. Each is an answer to a different counterfactual. Agreement strengthens a mechanism claim. Disagreement reveals interaction structure that a component leaderboard would hide.
Separate three questions
The central practical contribution of this paper is a Residual Contribution Audit. It separates three properties that are often collapsed:
Write asks whether a component changed the residual state in a specified direction or subspace. Survival asks whether that change remains available after later layers, rather than being rotated, overwritten or cancelled. Use asks whether changing that write changes the tested behaviour through the model’s own downstream computation.
A fourth diagnostic, recovery, asks whether restoring the clean write into a corrupted run recovers the clean result. Recovery is especially informative because it tests a counterfactual carrier: can this state difference transmit the relevant information when placed back into the original computation?
The Residual Contribution Audit
1. Write
Measure the component output, its norm, direction and direct readout. Ask what changed at the write site.
2. Survival
Track the candidate direction through later residual states. Test rotation, amplification, cancellation and erasure.
3. Use
Ablate, resample or replace the write and measure the behavioural effect with non-target controls.
4. Recovery
Patch the clean write into a matched corrupted run. Ask whether the predicted output and intermediate route return.
Suppose attention writes a vector whose projection onto the “Paris minus Lyon” logit direction is +4.0. The next MLP writes -3.2 on the same direction. The final net support attributable to those two direct projections is +0.8. A simple cancellation index is 1 - |net| / (|positive| + |negative|). Here it equals 0.89.
The number does not prove that the components implement one semantic feature. It flags an interaction that a snapshot can miss. A high index tells the analyst to test erasure and compensation before ranking modules by direct contribution.
Intervene on matched runs
Causal tracing and activation patching compare a clean run with a matched corrupted run. An internal activation from one run is inserted into the other, and the effect on a defined metric is measured. Meng and colleagues used a causal intervention of this kind to identify states that mediated factual predictions in the GPT models they studied.[9]
Ablation and patching answer different questions. Ablation asks whether the current route is necessary under a chosen replacement, often zero or a resampled activation. Clean-to-corrupt patching asks whether a state is sufficient to restore some behaviour in the corrupted context. Neither establishes unique responsibility. Redundant routes can hide necessity. A large patch can carry collateral information. Off-manifold replacements can create damage that the endogenous system never encounters.
Build the rival generator before trusting the result
A causal-looking pattern can be produced by several mechanisms. The practical defence is to name the strongest rival before running the intervention, then add the control that would separate it from the preferred explanation.
| Observed result | Rival generator | Discriminating control |
|---|---|---|
| Ablation has little effect | A redundant route carries the same function. | Test paired or path-specific ablations and look for compensatory writes. |
| Ablation has a large effect | Zeroing created an off-distribution state. | Compare zero, mean, resampled and reconstruction-preserving replacements. |
| A patch restores the target | The patch transplanted broad correlated information. | Measure non-target drift and patch a narrower subspace or token site. |
| An answer appears early | The final decoder happens to align with an unfinished state. | Compare a tuned translator, suffix compatibility and downstream intervention. |
| A component ranks highly | Normalisation or basis choice inflated its direct projection. | Recompute through the exact output path and test stable effects across bases and prompts. |
This discipline changes a negative result as well. A failed patch does not mean the information was absent. It may mean the patch site was too broad, too narrow, mistimed or incompatible with the corrupted context. The result lowers one specified mechanism claim, not every possible account of the behaviour.
At layer six, a logit lens already predicts “Paris”. Freeze that residual state and attach the model’s final unembedding. The answer looks complete. Now replace layers seven to twelve with a separately trained suffix that expects a rotated representation. The same layer-six state no longer yields Paris through the suffix, even though the direct lens still does.
What changed? Not the information visible to the external decoder. What changed was compatibility with the model’s downstream consumers. Linear readability is a relation between a state and a decoder; causal use is a relation between a state and the computation that actually follows it.
For behaviours with strong cancellation or redundancy, a component ranking that combines patch recovery, survival and pairwise interaction will predict out-of-sample behavioural effects better than direct logit attribution alone. The smallest useful test is a synthetic task with known circuits and deliberately trained eraser components.
The hypothesis gains support if the combined audit predicts held-out intervention effects across prompts and seeds. It weakens if direct readout performs equally well once normalisation is handled correctly. A rival explanation is that any gain comes from using more measurements rather than from the write-survival-use distinction itself.
stateDiagram-v2 [*] --> Claim state "Begin with a river, not a pipeline" as Claim state "Computation as a dye stream" as Test state "One state, three causal views" as Revision Claim --> Test: expose mechanism Test --> Decision: evidence survives Test --> Revision: rival survives Revision --> Claim: narrow or rebuild Decision --> [*]
Build the block so it can answer for itself
The most useful implementation exposes intermediate state, component writes and counterfactual controls without changing the model’s normal forward path.
Five hook points are enough to begin
For each block, record the incoming residual state, the attention write, the post-attention state, the feed-forward write and the outgoing state. Record normalised reads when the question concerns what a sublayer detected. Bind every trace to the exact model weights, input tokens, mask, normalisation configuration and decoding metric.
Do not log only attention weights. They show how value vectors were mixed, not the full vector written after value and output projections. Do not log only hidden states. Their differences conflate all writes between two sites. A trace becomes causally useful when it preserves both the state and the delta that changed it.
The smallest valid audit packet
A convincing residual experiment needs more than tensors. It needs a contract that makes the counterfactual reproducible. The smallest useful packet contains four linked records.
Baseline pair. Store a clean input and a minimally corrupted input that differ in one declared causal property. Verify that the output metric changes before touching the model internally.
Intervention contract. Name the model version, layer, token positions, component, tensor shape, replacement source and timing. “Ablate head 3” is incomplete until zeroing, mean replacement, resampling or another rule is specified.
Metric bundle. Record the target metric, at least one non-target behaviour, the full output distribution where feasible, and intermediate survival measures. A patch that recovers one logit while scrambling everything else is not selective evidence.
Scope receipt. Repeat across prompts, seeds and matched controls, then state the population to which the result applies. Version normalisation, tokenizer, mask, precision and decoding because each changes the receiving computation.
Two invariants make the packet auditable. The clean and corrupted runs must use the same frozen parameters and execution settings. The intervention must alter only the declared tensor sites. Hashes or deterministic checks can verify both. A positive result then permits a narrow statement about causal leverage for that metric and context. A negative result lowers that statement, but cannot establish global absence or lack of representation.
This packet is intentionally smaller than a full interpretability platform. It can be implemented with forward hooks, saved activations and a metric function. Its value lies in forcing the analyst to expose assumptions that prose otherwise hides.
A hooked block inside a controlled experiment
A realistic configured example
The executable lab trains a one-block, one-task decoder on synthetic sequences. The last position contains a fixed query token; the target is the first token. Causal attention permits the query position to read earlier positions. The model is small enough to train on a CPU in seconds, yet it contains token and positional embeddings, pre-LN multi-head attention, a GELU MLP, residual additions, a final normaliser and tied unembedding.
On the fixed seed supplied below, the model reaches perfect batch accuracy on the synthetic task. For the held-out sequence [7, 3, 9, 5, 0], the target probability is near zero at the embedding readout, 0.86 after the attention write, and approximately 1 after the MLP. Attention ablation reduces target probability to about 0.007. MLP ablation leaves the correct top token with probability 0.86. Patching the clean attention write into a corrupted sequence restores the clean answer.
Those numbers establish only what the configured experiment tests: attention carries the first-token information to the query position in this trained model, and the MLP sharpens the readout. They do not establish a general role for attention or MLPs in language models.
Move two writes and watch one decision change
The browser lab is a causal skeleton of one token position. The horizontal coordinate supports token A; the vertical coordinate supports token B. Change the attention and MLP writes, then ablate either branch. The readout updates immediately.
- After attention
- Final state
- Preferred token
- Cancellation index
Residual writes move a token decision
Positive result: a branch changes the chosen token or recovers it under a patch. Negative result: the tested branch lacks causal leverage under this replacement and context. The toy cannot establish how a trained language model represents any real concept.
Promote a component from “interesting” to “mechanistic” only after its readout, ablation and patch results agree under matched controls. When they disagree, report the disagreement. It is often the most informative result.
Run the full Python experiment
Save the code as residual_stream_lab.py, install PyTorch, and run it with Python 3.10 or later. It trains only on generated integer sequences. No external dataset or model download is required.
Full runnable PyTorch implementation
from __future__ import annotations
import statistics
import torch
from torch import nn
import torch.nn.functional as F
# Reproducible, synthetic experiment.
torch.manual_seed(7)
torch.set_num_threads(1)
VOCAB = 12
SEQ = 5
D_MODEL = 32
HEADS = 4
class Block(nn.Module):
def __init__(self, d_model: int = D_MODEL) -> None:
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = nn.MultiheadAttention(
d_model, HEADS, batch_first=True
)
self.ln2 = nn.LayerNorm(d_model)
self.mlp = nn.Sequential(
nn.Linear(d_model, 4 * d_model),
nn.GELU(),
nn.Linear(4 * d_model, d_model),
)
def forward(
self,
x: torch.Tensor,
*,
ablate_attn: bool = False,
ablate_mlp: bool = False,
patch_attn: torch.Tensor | None = None,
trace: dict[str, torch.Tensor] | None = None,
) -> torch.Tensor:
r0 = x
z = self.ln1(r0)
causal_mask = torch.triu(
torch.ones(SEQ, SEQ, dtype=torch.bool), diagonal=1
)
attn_write, _ = self.attn(
z, z, z, attn_mask=causal_mask, need_weights=False
)
if ablate_attn:
attn_write = torch.zeros_like(attn_write)
if patch_attn is not None:
attn_write = attn_write.clone()
attn_write[:, -1, :] = patch_attn
r1 = r0 + attn_write
mlp_write = self.mlp(self.ln2(r1))
if ablate_mlp:
mlp_write = torch.zeros_like(mlp_write)
r2 = r1 + mlp_write
if trace is not None:
trace.update(
r0=r0.detach(),
attn_write=attn_write.detach(),
r1=r1.detach(),
mlp_write=mlp_write.detach(),
r2=r2.detach(),
)
return r2
class TinyCopyModel(nn.Module):
"""At the final query position, predict the first token."""
def __init__(self) -> None:
super().__init__()
self.token_embedding = nn.Embedding(VOCAB, D_MODEL)
self.position = nn.Parameter(torch.randn(SEQ, D_MODEL) * 0.02)
self.block = Block()
self.final_norm = nn.LayerNorm(D_MODEL)
self.unembed = nn.Linear(D_MODEL, VOCAB, bias=False)
self.unembed.weight = self.token_embedding.weight
def forward(self, tokens: torch.Tensor, **block_kwargs) -> torch.Tensor:
x = self.token_embedding(tokens) + self.position
x = self.block(x, **block_kwargs)
return self.unembed(self.final_norm(x[:, -1, :]))
def readout(self, residual: torch.Tensor) -> torch.Tensor:
return self.unembed(self.final_norm(residual[:, -1, :]))
def synthetic_batch(size: int = 128) -> tuple[torch.Tensor, torch.Tensor]:
tokens = torch.randint(0, VOCAB, (size, SEQ))
tokens[:, -1] = 0 # fixed query marker
target = tokens[:, 0].clone()
return tokens, target
def train_copy_model(steps: int = 700) -> TinyCopyModel:
model = TinyCopyModel()
optimiser = torch.optim.AdamW(model.parameters(), lr=2e-3)
for step in range(steps):
tokens, target = synthetic_batch()
logits = model(tokens)
loss = F.cross_entropy(logits, target)
optimiser.zero_grad()
loss.backward()
optimiser.step()
if step in {0, 99, 299, steps - 1}:
accuracy = (logits.argmax(-1) == target).float().mean().item()
print(
f"step={step + 1:3d} loss={loss.item():.4f} "
f"batch_accuracy={accuracy:.3f}"
)
return model.eval()
def audit(model: TinyCopyModel) -> None:
clean = torch.tensor([[7, 3, 9, 5, 0]])
corrupt = torch.tensor([[2, 3, 9, 5, 0]])
trace: dict[str, torch.Tensor] = {}
with torch.no_grad():
clean_logits = model(clean, trace=trace)
corrupt_logits = model(corrupt)
no_attention = model(clean, ablate_attn=True)
no_mlp = model(clean, ablate_mlp=True)
patched = model(
corrupt,
patch_attn=trace["attn_write"][:, -1, :],
)
print("\nLayer-wise readouts for target token 7")
stages = {
"embedding": trace["r0"],
"after attention": trace["r1"],
"after MLP": trace["r2"],
}
for name, residual in stages.items():
logits = model.readout(residual)
probability = logits.softmax(-1)[0, 7].item()
print(
f"{name:16s} p(target)={probability:.3f} "
f"top={logits.argmax(-1).item()}"
)
print("\nInterventions")
cases = {
"clean": clean_logits,
"attention ablated": no_attention,
"MLP ablated": no_mlp,
"corrupt": corrupt_logits,
"corrupt + clean attention patch": patched,
}
for name, logits in cases.items():
probability = logits.softmax(-1)[0, 7].item()
print(
f"{name:34s} p(target)={probability:.3f} "
f"top={logits.argmax(-1).item()}"
)
print("\nWrite norms at the query position")
print("attention", trace["attn_write"][0, -1].norm().item())
print("MLP ", trace["mlp_write"][0, -1].norm().item())
# Optional: reproduce Figure 5 with matched synthetic stacks.
class FFOperator(nn.Module):
def __init__(self, d_model: int) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Linear(d_model, 4 * d_model),
nn.GELU(),
nn.Linear(4 * d_model, d_model),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.net(x)
class PreLNBlock(nn.Module):
def __init__(self, d_model: int) -> None:
super().__init__()
self.n1, self.n2 = nn.LayerNorm(d_model), nn.LayerNorm(d_model)
self.a, self.m = FFOperator(d_model), FFOperator(d_model)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x + self.a(self.n1(x))
return x + self.m(self.n2(x))
class PostLNBlock(nn.Module):
def __init__(self, d_model: int) -> None:
super().__init__()
self.n1, self.n2 = nn.LayerNorm(d_model), nn.LayerNorm(d_model)
self.a, self.m = FFOperator(d_model), FFOperator(d_model)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.n1(x + self.a(x))
return self.n2(x + self.m(x))
def gradient_profile(seeds: int = 30, depth: int = 18, d_model: int = 48):
medians: dict[str, list[float]] = {}
for name, block_type in {"pre": PreLNBlock, "post": PostLNBlock}.items():
runs: list[list[float]] = []
for seed in range(seeds):
torch.manual_seed(seed)
blocks = nn.ModuleList([block_type(d_model) for _ in range(depth)])
output = nn.Linear(d_model, d_model)
x = torch.randn(8, 6, d_model)
target = torch.randn_like(x)
y = x
for block in blocks:
y = block(y)
loss = F.mse_loss(output(y), target)
loss.backward()
runs.append([
statistics.mean(
p.grad.detach().norm().item()
for p in block.parameters()
if p.grad is not None
)
for block in blocks
])
medians[name] = [
statistics.median(run[layer] for run in runs)
for layer in range(depth)
]
print("\nMedian gradient profiles")
for name, values in medians.items():
print(name, [round(value, 5) for value in values])
if __name__ == "__main__":
trained_model = train_copy_model()
audit(trained_model)
gradient_profile()
What it tests: layer-wise readout, attention and MLP ablation, clean-to-corrupt patch recovery, component write magnitude, and a matched pre-LN versus post-LN gradient illustration. What it cannot establish: a general decomposition of language-model cognition, semantic localisation, or unique causal responsibility.
Expected output and interpretation
Exact decimals can vary with PyTorch version, but the fixed seed should produce the same qualitative pattern: training reaches near-perfect accuracy; the target becomes readable after attention; attention ablation destroys the answer; MLP ablation reduces confidence; and patching the clean attention write into the corrupted run restores the target. The gradient profile should show distinct depth shapes for the two layouts.
A positive patch result permits a scoped statement: the patched state is a sufficient carrier for recovering the chosen metric in the corrupted context. A negative patch result means this site, replacement and metric did not restore the behaviour. It does not prove the information was absent, because it may be distributed, encoded elsewhere or incompatible with the receiving context.
The strongest alternative explanation
The residual-stream view can itself become too coarse. A component output is a vector that may superpose many features. One block may contain several circuits with opposing effects. Layer boundaries are engineering units, not guaranteed cognitive units. Gated residuals, mixture-of-experts MLPs, parallel attention-MLP layouts and normalisation-free variants change the exact contract.
The alternative is not to abandon the residual view. It is to treat it as a measurement coordinate. It tells us where state persists and where updates enter. Feature-level explanations still require finer decompositions, controlled interventions and stability across inputs. The residual stream is a causal workspace in the architectural sense, not a certificate that its axes correspond to human concepts.
sequenceDiagram participant Q as Question participant M as Mechanism participant E as Evidence participant D as Decision Q->>M: The answer that appeared between two additions M->>E: Begin with a river, not a pipeline E-->>M: compare rival explanations E->>D: Treat the block as an auditable update contract D-->>Q: revise scope or act
Treat the block as an auditable update contract
When reading or designing a transformer, stop asking which module “contains the representation” as though attention and the MLP pass ownership of a single object. Ask which state persists, which normalised view each sublayer reads, what vector it writes, and which later operations consume that change.
For architecture, this changes the unit of comparison. A block variant is not defined by a shopping list of attention, feed-forward layers, skips and normalisers. It is defined by their placement on the read, write and gradient paths. Moving one normaliser can change optimisation even when the named ingredients remain. Replacing LayerNorm with RMSNorm, DyT or another bounded operator changes invariances and sensitivity control, rather than only speed.
For interpretability, this changes evidential standards. Intermediate logits are excellent hypothesis generators. Component norms and direct projections are useful diagnostics. Neither proves use. Apply the Residual Contribution Audit: establish the write, test its survival, intervene on its use, and seek recovery in a matched counterfactual. Report cancellation and collateral drift rather than hiding them behind one attribution score.
For implementation, this changes observability. Expose state and delta separately at every block boundary. Version the exact normalisation and decoding path. Preserve clean and corrupted traces. Make ablation and patching ordinary test operations rather than one-off research surgery.
The decision is therefore concrete: treat the transformer block as an auditable update contract over shared state. Design it by tracing read and write paths. Explain it by interventions on those paths. Release claims only at the level the evidence supports. The residual stream makes deep composition possible, but only a causal audit tells us which currents actually moved the answer.
Glossary
- Residual stream
- The evolving token-state matrix carried across blocks by residual additions.
- Residual write
- The vector or token-state update produced by an attention or feed-forward sublayer and added to the stream.
- Skip path
- The identity route that carries the prior state around a learned sublayer.
- Pre-LN
- A block layout in which normalisation is applied before the learned sublayer inside the residual branch.
- Post-LN
- A block layout in which normalisation follows the residual addition.
- Direct readout
- A projection of an intermediate state or component write into an output space, often vocabulary logits.
- Survival
- The extent to which a candidate write remains available after later layers transform the residual state.
- Activation patching
- Replacing an internal activation in one run with the corresponding activation from a matched run and measuring the effect.
- Patch recovery
- The restoration of a clean metric when a clean internal state is inserted into a corrupted computation.
- Cancellation index
- A diagnostic for opposing direct projections; it flags interaction but does not identify a semantic feature.
References
- Vaswani, A. et al. (2017). Attention Is All You Need. Advances in Neural Information Processing Systems 30.
- He, K., Zhang, X., Ren, S. and Sun, J. (2016). Deep Residual Learning for Image Recognition. CVPR.
- Ba, J. L., Kiros, J. R. and Hinton, G. E. (2016). Layer Normalization.
- Xiong, R. et al. (2020). On Layer Normalization in the Transformer Architecture. ICML.
- Zhang, B. and Sennrich, R. (2019). Root Mean Square Layer Normalization. NeurIPS.
- Elhage, N. et al. (2021). A Mathematical Framework for Transformer Circuits. Transformer Circuits Thread.
- Geva, M., Schuster, R., Berant, J. and Levy, O. (2021). Transformer Feed-Forward Layers Are Key-Value Memories. EMNLP.
- Belrose, N. et al. (2023). Eliciting Latent Predictions from Transformers with the Tuned Lens.
- Meng, K., Bau, D., Andonian, A. and Belinkov, Y. (2022). Locating and Editing Factual Associations in GPT. NeurIPS.
- Janiak, J., Rager, C., Dao, J. and Lau, Y. T. (2024). An Adversarial Example for Direct Logit Attribution: Memory Management in GELU-4L. BlackboxNLP.
- Kim, J. et al. (2025). Peri-LN: Revisiting Normalization Layer in the Transformer Architecture. ICML.
- Zhu, J., Chen, X., He, K., LeCun, Y. and Liu, Z. (2025). Transformers without Normalization. CVPR.
- Chen, M. et al. (2026). Stronger Normalization-Free Transformers. CVPR.