Skip to main content

Points and addresses

A point is a named tensor in the forward pass. An address is a point plus the coordinates that pick out one of them, and it is what every capture and steering call takes.

Three coordinates

from interp_engine import Address

Address("resid_post", 10) # name and layer
Address("embeddings") # trunk-level, so no layer
Address("resid_streams", 5, 2) # name, layer, stream
Address("model.layers.5.mlp.down_proj") # a module path, for what the table below does not name

name is a point from the table below, or a dotted module path. The point set is open: an unrecognized name with no layer is looked up as a module, which is how you tap something the engine does not enumerate.

layer is position in flattened forward order — one total order over sublayer executions in a single forward pass, rather than a block index plus a sublayer index. So no tensor has two spellings.

stream is which parallel residual stream, on a hyper-connection trunk. It is the only coordinate that is a real tensor axis: all of them exist at once in a [batch, pos, streams, d_model] activation, so nothing can flatten them the way execution order flattens.

The field set is closed, so no family can invent a coordinate of its own.

The string form

Every address round-trips through one string, and a Cache accepts either form on lookup.

from interp_engine import Address, format_address, parse_address, to_address

format_address(Address("resid_post", 5, 2)) # "resid_post.5.stream-2"
parse_address("mlp_act.10")
to_address(("z", 7)) # the older tuple form still reads

Parsing is strict, and that is the useful part. A coordinate selects a tensor rather than annotating one, so a parser that skipped stream-2 in resid_post.5.stream-2 would hand back resid_post.5 — a real tensor of the right shape from the wrong place. An unrecognized coordinate raises UnknownCoordinate instead, which a caller can catch to report version skew specifically. The legacy "resid_post:5" spelling raises too.

Only unreserved URL characters are emitted, so an address needs no escaping in a query string, no quoting in a shell, and is a legal filename everywhere.

The 34 points

27 on every model, and 7 more that need a hyper-connection trunk. Width is what the last axis counts.

pointwidthwhat it is
embeddingsd_modelthe embedding module's output. Trunk-level, so no layer
resid_pred_modelthe residual entering the block
attn_ind_modelwhat the attention module was handed
q_norm_in / q_norm_outn_heads * head_dimthe query on either side of QK-norm
k_norm_in / k_norm_outn_kv_heads * head_dimthe key on either side of QK-norm
valuen_heads * head_dimthe value projection
attn_scoresn_heads * query * keythe QK matrix, before the softmax
attn_probsn_heads * query * keythe attention pattern, after it
zn_heads * head_dimper-head attention output, before W_O
attn_gaten_heads * head_dimthe raw double-width projection, on a gated-attention model
attn_outd_modelattention's raw module output
attn_out_postd_modelattention's residual contribution
resid_midd_modelthe input to the pre-MLP norm
mlp_ind_modelwhat the MLP was handed
mlp_pred_mlppre-activation neurons
mlp_pre_lineard_mlpthe ungated branch, on gated MLPs
mlp_actd_mlppost-activation neurons
router_logitsn_expertsthe MoE router's output
expert_weightsn_expertsthe top-k gate weights, in the router's ranking order
expert_indicesn_expertswhich experts were picked; the one integer-valued point
mlp_outd_modelthe MLP's raw module output
mlp_out_postd_modelthe MLP's residual contribution
resid_postd_modelthe residual leaving the block
final_normd_modelthe trunk's last norm. Trunk-level
lm_headvocab_sizethe bare unembed. Trunk-level

Reach for the _post point when you want a sublayer's contribution to the residual. It is the same tensor as the raw output on most families and differs on a sandwich-norm one (Gemma-2/3, OLMo-2/3), where the raw output has not been through the post-sublayer norm yet. attn_out_post and mlp_out_post alias the raw points wherever that norm does not exist, so asking for the contribution is safe everywhere — and this is the mapping mistake that produces a plausible-looking wrong number rather than an error.

Only on a hyper-connection trunk

DeepSeek-V4 and Motif 3 today, and any family whose config reports more than one residual stream. These are gated on that count rather than on an architecture name.

pointwidthwhat it is
resid_streamsn_residual_streamsthe block's own output stack; stream k is stack[:, k]
attn_stream_collapsed_modelthe one d_model vector attention reads
mlp_stream_collapsed_modelthe same for the FFN, one norm before mlp_in
attn_stream_writen_residual_streamsthe per-stream weights attention's output is written back with
attn_stream_mixn_residual_streamsthe doubly-stochastic matrix that remixes the streams after it
mlp_stream_writen_residual_streamsthe MLP's counterpart
mlp_stream_mixn_residual_streamsthe MLP's mixing matrix

The two collapse points are what an SAE or a steering vector wants on such a trunk, since they are the vectors a sublayer actually reads. The four write and mix rows are coefficients rather than activations, so they capture but refuse a steer.

Note that resid_streams and the stream coordinate say different things. resid_streams.5 is the whole stack, [tokens, streams, d_model]; resid_post.5.stream-2 qualifies a residual point with one stream and is eager-only.

Which backend serves which

This page is the vocabulary, not the support matrix — every point above is capturable eagerly, and the vLLM backend serves most but not all of them. Ask the model rather than a table if you are branching on it, with model.points(); see Capabilities. The full per-backend breakdown, with a reason for each refusal, is in SUPPORTED_POINTS.md, and the visualizer shows where each point sits in the forward pass.