Locating activations¶
Lens, patching and probing read activations at locations a locator names: the source of a
lens config, the source and target of every patch element, and the source of a probe config. A
locator is a ModelLocatorConfig. Resolving it against the model and the prompt gives the run’s index: one
row per location, which is also one row of the result.
The examples on this page build locators in code, resolve them against GPT-2 and print the rows they give. A config file writes the same locators with the enums’ values, as the YAML blocks show:
from transformer_lens.model_bridge import TransformerBridge
from entoscope import ModelAxis, ModelLayerLocation, ModelLocatorConfig
from entoscope.model.index import Index
model = TransformerBridge.boot_transformers("gpt2")
model.eval()
def rows(locator: ModelLocatorConfig, n_positions: int = 6):
"""The rows a locator resolves to for a prompt of n_positions tokens, as index.csv holds them."""
return Index(locator, locator.parse(model, n_positions)).to_df()
locator = ModelLocatorConfig(
activation=ModelLayerLocation.RESIDUAL_POST,
location=[ModelAxis.LAYER, ModelAxis.POSITION],
)
print(rows(locator)) # 72 rows: 12 layers x 6 positions
Activations¶
activation names the activation a row reads, a ModelLayerLocation member. Each one is a
transformer_lens activation, cached under its transformer activation name:
|
|
Axes of the tensor |
Transformer activation name (in layer 0) |
Size of |
|---|---|---|---|---|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, head, neuron |
|
|
|
|
batch, position, head, neuron |
|
|
|
|
batch, position, head, neuron |
|
|
|
|
batch, head, dest_pos, src_pos |
|
none |
|
|
batch, position, head, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
|
|
batch, position, neuron |
|
|
A few of them have quirks:
embedsits before the first block, so its locator names nolayer, and one that does is refused.attn.patternis located bylayer,head,dest_posandsrc_pos. Entry[dest_pos, src_pos]is the weight the token atdest_pos(the query) gives the token atsrc_pos(the key). Attention is causal, so every entry withsrc_pospastdest_posis 0 in every run. The pattern has nopositionorneuronaxis, so a locator naming either for it is refused rather than reading the same whole pattern on every row.attn.kandattn.vhaven_key_value_headsheads on a model with grouped-query attention.mlp.pre_linearexists only on a gated MLP, where it is the linear branch andmlp.prethe gate.attn.inandmlp.inare what the sublayer itself reads, after the norm before it on a model that has one. Patching one changes that sublayer’s input alone, even where the block hands the same tensor on elsewhere: to the other sublayer of a parallel block whose attention and MLP share one norm (GPT-J), or as the residual stream itself on a model with no norm before the sublayer (OLMo 2).attn.outandmlp.outare what the sublayer adds to the residual stream: after the norm that follows it on a model that norms a sublayer’s output (Gemma 2 and 3 and OLMo 2, for instance).attn.out_pre_normandmlp.out_pre_normare its output before that norm, and the same tensor asattn.outandmlp.outon any other model.attn.in,mlp.in,attn.out_pre_normandmlp.out_pre_normare hooks on aTransformerBridge’s sublayers, which aHookedTransformerdoes not have.
A locator is refused when the model computes no transformer activation it reads, as an operation resolves
its index and so before any forward pass: one of those four on a HookedTransformer, mlp.pre_linear
on a model without a gated MLP, resid.mid on a parallel block, or a block’s activation in a row that
names several layers, or none.
Axes¶
A row names a value on some of six axes, the members of ModelAxis:
layer(ModelAxis.LAYER):0ton_layers - 1.head(ModelAxis.HEAD):0ton_heads - 1.neuron(ModelAxis.NEURON): the activation’s last axis, sized as in the table above.position(ModelAxis.POSITION):0to the prompt’s token count minus one.model.to_tokensprepends a BOS token, so position 0 is BOS and a prompt’s first word is at position 1.dest_pos(ModelAxis.DESTINATION_POSITION) andsrc_pos(ModelAxis.SOURCE_POSITION): the query and the key position ofattn.pattern, numbered aspositionis. The names are TransformerLens’s.
A negative value counts from the end of its axis: position: -1 is the last token and layer: -1 the
last layer. A value past either end of its axis is refused when the index is resolved, by plan() as
by a run, so layer: 12 on GPT-2’s twelve layers fails before any forward pass. An axis a row does not
name is read whole: every row of {activation: resid.post, location: [layer]} spans every position.
A locator that names any axis needs an activation to read it in, set on the locator or on every
coordinate, and may name only the axes of that activation’s tensor, plus layer for every activation
but embed: head on resid.post, say, is refused as the config is read.
Two ways to name locations¶
A locator names its rows in one of two ways, never both:
Declaratively:
locationlists axes, every value of each is enumerated, andfilter,group,incrementalandcombinationnarrow and reshape the grid.Explicitly:
coordinateslists the exact rows.
Declarative: axes, then a filter¶
location lists the axes the index enumerates, and the index is every combination of their values:
[layer, head] on attn.z is 12 × 12 rows for GPT-2. A filter then keeps the rows matching any of
its terms. Within a term every key must match, and a term {not: ...} drops the rows it matches
instead. A constraint is an int, {range: [start, end]} with both ends included, or a list of these:
from entoscope import Filter
locator = ModelLocatorConfig(
activation=ModelLayerLocation.ATTENTION_Z,
location=[ModelAxis.LAYER, ModelAxis.HEAD, ModelAxis.POSITION],
filter=Filter.from_config(
[
{ModelAxis.LAYER: {"range": [0, 5]}, ModelAxis.POSITION: -1}, # layers 0-5, last position only
{ModelAxis.LAYER: 11}, # or layer 11, every position
{"not": {ModelAxis.HEAD: 0}}, # minus every row of head 0
]
),
)
print(len(rows(locator))) # 132: (6 layers x 1 position + 1 layer x 6 positions) x 11 heads
The same locator in a config file:
source:
activation: attn.z
location: [layer, head, position]
filter:
- layer: {range: [0, 5]}
position: -1
- layer: 11
- not: {head: 0}
An attention pattern is located entry by entry the same way. This locator reads how much the last token attends to each position, in layer 9’s head 8:
locator = ModelLocatorConfig(
activation=ModelLayerLocation.ATTENTION_PATTERN,
location=[ModelAxis.LAYER, ModelAxis.HEAD, ModelAxis.DESTINATION_POSITION, ModelAxis.SOURCE_POSITION],
filter=Filter.from_config([{ModelAxis.LAYER: 9, ModelAxis.HEAD: 8, ModelAxis.DESTINATION_POSITION: -1}]),
)
print(len(rows(locator))) # 6: dest_pos 5, src_pos 0 to 5
Reading several values together: group, incremental, combination¶
A row can hold a list of values on an axis. It then reads all of them together. A lens sums a group of
heads or neurons into one direction, a patch writes the whole group in one run, and a probe takes
every one of them as features. A row holding lists on several axes reads every combination of
their values: dest_pos: [4, 5] with src_pos: [1, 4] reads the four entries of that block of the
pattern. An axis the row reads whole stays whole within the block, even between two listed ones:
head: [8, 9] with src_pos: [1, 4] reads both heads’ weights on keys 1 and 4 from every dest_pos.
Three transforms build such rows, applied after the filter in this order:
groupmerges the listed values of an axis into one row;allmerges every value. The axis has to be onelocationenumerates.incrementalfollows every row with rows that add growing prefixes of the listed values, adding the axis whenlocationdoes not name it.combinationexpands every row into one row per combination of the values it holds on the listed axes, from one value up tomax_elements. Each axis has to be onelocationorincrementalnames.
from dataclasses import replace
from entoscope import Combination, Group, Incremental
heads = ModelLocatorConfig(activation=ModelLayerLocation.ATTENTION_Z, location=[ModelAxis.LAYER, ModelAxis.HEAD])
print(rows(replace(heads, filter=Filter.from_config([{ModelAxis.LAYER: 9}]), group=Group({ModelAxis.HEAD: [6, 9]}))))
# 11 rows: one reading heads 6 and 9 together, and one for each other head of layer 9
print(
rows(
replace(
heads,
filter=Filter.from_config([{ModelAxis.LAYER: 9, ModelAxis.HEAD: 0}]),
incremental=Incremental({ModelAxis.HEAD: [6, 9]}),
)
)
)
# 3 rows: heads [0], [0, 6] and [0, 6, 9]
print(
rows(
replace(
heads,
filter=Filter.from_config([{ModelAxis.LAYER: 9, ModelAxis.HEAD: [6, 9, 10]}]),
group=Group({ModelAxis.HEAD: "all"}),
combination=Combination(location=[ModelAxis.HEAD], max_elements=2),
)
)
)
# 6 rows: [6], [9], [10], [6, 9], [6, 10], [9, 10]
Every layer is a transformer activation of its own, so a row reads one layer. Group heads, neurons and positions, but not layers: a row that names several layers, or none, is refused.
Explicit: coordinates¶
coordinates lists the rows themselves, each a mapping from axis to value. Every coordinate uses the
same keys, a value may be a list to read several entries together, and negative values count from
the end. A coordinate may name its own activation, and then every coordinate must, while the
locator’s activation stays unset:
from entoscope import Coordinates
locator = ModelLocatorConfig(
coordinates=Coordinates(
[
{ModelAxis.ACTIVATION: ModelLayerLocation.RESIDUAL_POST, ModelAxis.LAYER: -2, ModelAxis.POSITION: -1},
{ModelAxis.ACTIVATION: ModelLayerLocation.MLP_OUT, ModelAxis.LAYER: -1, ModelAxis.POSITION: -1},
]
)
)
print(rows(locator))
# 2 rows, in the order listed: resid.post at layer 10 and mlp.out at layer 11, both at position 5
Coordinates cannot be combined with location, filter or the transforms.
Prompts of different lengths¶
A lens or a patching run resolves its positions against its prompt. Probing reads many prompts, which may tokenize to different lengths, and then positions stay symbolic. A non-negative position counts from the start of every prompt, and a negative one from the end of each prompt in turn. Three things follow:
An unfiltered
position,dest_posorsrc_posaxis is rejected, since its size would depend on the prompt. Filter it to explicit positions, or leave it out oflocationand pool over it (see Probing).A
rangemust stay on one side:{range: [-3, -1]}works,{range: [1, -1]}does not.Every explicit position must exist in the shortest prompt.
From index to result¶
The index is written to index.csv and makes up the first columns of result.csv. Every axis
becomes a column prefixed source_ (and target_ for the target side of a patch), with the
activation in source_activation. A row’s list of values is written as a list, such as [6, 9]. The
operation’s own columns follow; each operation page lists them.