transformer_lens.tools.analysis.attribution_patching module¶
Attribution patching — linearized activation patching on TransformerBridge.
Attribution patching estimates the causal effect of every model component on a
task metric with a gradient-based linearization of activation patching: rather
than one forward pass per intervention, it reads a single gradient cache. For a
clean/corrupt prompt pair it runs a clean forward (for a_clean) and a corrupt
forward whose backward hooks capture g = d(metric)/d(a) (for a_corrupt and
its gradient), and scores each node with the
first-order Taylor estimate effect(node) = (a_clean - a_corrupt) . g. Scores
over a batch of clean/corrupt pairs are averaged before ranking.
Only the TransformerBridge API is targeted; TransformerLens v4 deprecates
HookedTransformer.
Sign/direction convention (denoising form): the gradient is taken on the corrupt run and the estimate points toward the clean activation, so a positive score means patching that node from corrupt toward clean moves the metric in the positive direction. An oracle-parity test maps this convention onto a pinned reference rather than assuming the two agree.
Memory note: gradients are retained only for hook points passing names_filter.
Retaining gradients at every hook point roughly doubles cache memory, so callers
should filter to the hook families their analysis actually reads.
Scope: this build ships node granularity with plain attribution (ig_steps=1).
Edge scoring (EAP), the integrated-gradient path (EAP-IG, ig_steps>1), and
ablate-outside faithfulness are not implemented yet; their API is declared here —
granularity="edge" and ig_steps>1 raise NotImplementedError — so
downstream code can pin against a stable surface now.
- class transformer_lens.tools.analysis.attribution_patching.AttributionResult(node_scores: dict[~transformer_lens.tools.analysis.attribution_patching.Node, float], edge_scores: dict[tuple[~transformer_lens.tools.analysis.attribution_patching.Node, ~transformer_lens.tools.analysis.attribution_patching.Node], float] = <factory>)¶
Bases:
objectScored output of an attribution-patching sweep.
- node_scores¶
Signed first-order effect estimate per node,
(a_clean - a_corrupt) . d(metric)/d(a). A positive score means patching that node from corrupt toward clean moves the metric in the positive direction (the denoising convention pinned in the module docstring).- Type:
dict[transformer_lens.tools.analysis.attribution_patching.Node, float]
- edge_scores¶
Per-edge effect estimate keyed by
(source, destination). Declared here so the result API is stable across the PR series; it is populated only once edge scoring lands and is empty for a node sweep.
- top_edges(k: int = 10) list[tuple[Node, Node, float]]¶
The
khighest-magnitude edges — populated once edge scoring lands.
- top_nodes(k: int = 10) list[tuple[Node, float]]¶
The
knodes with the largest effect magnitude, strongest first.Ranking is by absolute score: a node with a large negative effect is as causally important as one with a large positive effect, so magnitude — not signed value — orders the circuit. Ties keep enumeration order (stable sort). Requesting more than the available nodes returns all of them.
- class transformer_lens.tools.analysis.attribution_patching.EdgeAttributionConfig(granularity: Literal['node', 'edge'] = 'node', ig_steps: int = 1)¶
Bases:
objectConfiguration for an attribution-patching sweep.
The two axes are deliberately orthogonal:
granularityselects what is scored:"node"scores each residual-stream write,"edge"scores each(source, destination)write->read pair.ig_stepsselects gradient fidelity.ig_steps=1is plain attribution patching / EAP: a single first-order Taylor gradient taken at the corrupt point.ig_steps>1is EAP-IG: the integrated gradient averaged over that many points along the corrupt->clean path, which corrects the gradient saturation that makes plain attribution unfaithful.
There is intentionally no
methodfield. An earlier design had both amethodenum ("attribution"/"EAP"/"EAP-IG") andig_steps, which overlap: the method is fully determined bygranularityand whetherig_stepsexceeds 1. Collapsing them removes the invalid states (e.g.method="attribution", ig_steps=5).This build implements node granularity with plain attribution only.
granularity="edge"andig_steps>1are accepted by the type but raiseNotImplementedErrorat construction, so downstream code can import and reference this API now while edge scoring and the integrated-gradient path are not implemented yet. Once EAP-IG lands, the default flips toig_steps=5(EAP-IG is the faithful default); until then the default is the only executable value,ig_steps=1.- granularity¶
"node"or"edge". Defaults to"node".- Type:
Literal[‘node’, ‘edge’]
- ig_steps¶
Integrated-gradient path steps (
>=1). Defaults to1.- Type:
int
- granularity: Literal['node', 'edge'] = 'node'¶
- ig_steps: int = 1¶
- class transformer_lens.tools.analysis.attribution_patching.GradientCache(activations: dict[str, Tensor], gradients: dict[str, Tensor | None], metric: Tensor)¶
Bases:
objectActivations and their metric-gradients from one forward + backward pass.
- activations¶
Detached activation tensor per cached hook name.
- Type:
dict[str, torch.Tensor]
- gradients¶
d(metric)/d(activation)per cached hook name.- Type:
dict[str, torch.Tensor | None]
- metric¶
The scalar metric value at this run (detached).
- Type:
torch.Tensor
- activations: dict[str, Tensor]¶
- gradients: dict[str, Tensor | None]¶
- metric: Tensor¶
- class transformer_lens.tools.analysis.attribution_patching.Node(kind: Literal['embed', 'attn_head_out', 'mlp_out'], position: int, layer: int | None = None, head: int | None = None)¶
Bases:
objectA node in the residual-stream computational graph at node granularity.
Nodes are the typed, hashable keys the attribution sweep scores. Each node is identified by
(kind, layer, position, head);kindselects the node family and constrains which oflayer/headapply:"embed": the token embedding write.layerandheadareNone."attn_head_out": one attention head’s output.layerandheadset."mlp_out": one layer’s MLP output.layerset,headisNone.
positionis the sequence index the node is read at. The invariants above are enforced in__post_init__so a malformed key raises rather than silently producing a wrong graph.- head: int | None = None¶
- property hook_name: str¶
The cache hook point this node reads from.
Uses the standard
TransformerBridgealias names (hook_embed,blocks.{l}.attn.hook_z,blocks.{l}.hook_mlp_out); the per-headattn_head_outnode slices headself.headout of the sharedhook_ztensor.
- kind: Literal['embed', 'attn_head_out', 'mlp_out']¶
- layer: int | None = None¶
- position: int¶
- transformer_lens.tools.analysis.attribution_patching.attribution_patch(model: Any, clean: Tensor, corrupt: Tensor, metric_fn: Callable[[Tensor], Tensor], config: EdgeAttributionConfig = EdgeAttributionConfig(granularity='node', ig_steps=1)) AttributionResult¶
Estimate every node’s causal effect on
metric_fnin two forwards + one backward.For each clean/corrupt pair this runs a clean forward (for
a_clean) and a corrupt forward whose backward hooks captureg = d(metric)/d(a)(fora_corruptand its gradient), then scores each node with the first-order Taylor estimateeffect(node) = (a_clean - a_corrupt) . g.Sign/direction convention (denoising form): gradients are taken on the corrupt run and the estimate points toward the clean activation, so a positive score means patching that node from corrupt toward clean moves the metric in the positive direction. An oracle-parity test maps this convention onto a pinned reference rather than assuming the two agree.
Dataset averaging:
clean/corruptmay hold a batch of prompt pairs. Each pair is scored independently (per-example forward/backward, so its own reconstruction identity holds) and per-node scores are averaged across the batch before ranking.- Parameters:
model – A
TransformerBridge(or compatible) exposingcfg.n_layers,hook_dict, andhooks().clean – Clean token ids, shape
[batch, seq].corrupt – Corrupt token ids, shape
[batch, seq], paired row-by-row withclean.metric_fn – Maps single-example logits to a scalar to differentiate.
config – Sweep configuration. This PR supports node granularity with plain attribution (
ig_steps=1) only; other values raise at construction.
- Returns:
An
AttributionResultwhosenode_scoresare averaged over the batch.edge_scoresstays empty until edge scoring lands.- Raises:
ValueError – if
clean/corruptare not 2D, hold a different number of pairs, or a pair tokenizes to different lengths (activations must align position-by-position).
- transformer_lens.tools.analysis.attribution_patching.cache_activation_and_gradient(model: Any, tokens: Tensor, metric_fn: Callable[[Tensor], Tensor], names_filter: str | Sequence[str] | Callable[[str], bool] | None = None, compute_gradient: bool = True) GradientCache¶
Run one forward and capture activations plus (optionally) their metric-gradients.
Registers a forward hook and a backward hook at each cached point, runs one grad-enabled forward, then drives a single backward with
torch.autograd.grad()to fire the backward hooks.run_with_cache(..., incl_bwd=True)only backpropagates the model’s own scalar output, so a custom (non-scalar-output) metric such as a logit-diff needs the backward driven here.Gradients come from the backward hooks, not from
.gradon the cached tensors.TransformerBridgereshapes the tensor handed to a forward hook at a converted point (attn.hook_z,hook_q/k/v,hook_attn_out), so that tensor is a view the model’s forward never consumes:retain_grad()on it is inert and its.gradstaysNone. A backward hook goes through the same conversion and delivers the real gradient in canonical shape. Driving the backward withtorch.autograd.grad()instead ofmetric.backward()keeps it off every parameter’s.gradbuffer — no caller grads clobbered, no model-sized buffer allocated.With
compute_gradient=Falsethe backward is skipped entirely: only forward hooks are registered, no backward is driven, and every cached point’s gradient isNone. Callers that read activations only (the clean pass ofattribution_patch(), which pairs these with the corrupt run’s gradients) use this to run a plain forward instead of a needless forward + backward.- Parameters:
model – A
TransformerBridge(or compatible) exposingcfg.n_layers,hook_dict, and thehooks()context manager.tokens – Input token ids for a single forward pass.
metric_fn – Maps the model logits to a scalar to differentiate.
names_filter – Restricts which hook points are cached (and have gradients retained).
None(the default) caches the node-granularity hook set —hook_embedplus each layer’sattn.hook_zandhook_mlp_out; pass an explicit filter to cache any hook point outside that set. On a real Bridge,Nonecannot mean “every hook point”: the gated points (hook_mlp_in,attn.hook_result, the split-QKV inputs) thathook_dictexposes raise inadd_hookunless theirset_use_*flag is on.compute_gradient – When
True(default) capture gradients via backward hooks. WhenFalserun an activation-only forward and leave every gradientNone.
- Returns:
A
GradientCachewith per-hook activations, and gradients whencompute_gradientisTrue(allNoneotherwise).
- transformer_lens.tools.analysis.attribution_patching.enumerate_nodes(model: Any, cache: GradientCache) list[Node]¶
Enumerate the full node-granularity graph from the Bridge hook graph.
The graph is explicit: for
n_layerslayers it always contains the embed write, every attention head’s output, and every layer’s MLP output, at every sequence position. Sequence length and head count are read from the cached tensor shapes;n_layersfrommodel.cfg.- Parameters:
model – A
TransformerBridge(or compatible) exposingcfg.n_layers.cache – A
GradientCacheholding at least the required hook points.
- Returns:
The node list, ordered embed-then-layerwise for deterministic ranking.
- Raises:
ValueError – if any required hook point is absent from
cache— the graph is never silently truncated.