Coverage for transformer_lens/model_bridge/sources/inspect/hooks.py: 100%
31 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-09-21 19:27 +0000
1"""Canonical hook names ↔ (layer, kind) for the Inspect HF provider.
3Torch-free; shared by the provider (capture/intervene) and the driver (supported set,
4decode). Covers the ``d_model``-shaped decoder-layer boundaries plus the head-split
5attention hooks (q/k/v/z, pattern) where the provider's structural probe finds the
6projections; ``attn_scores``, ``embed``, and ``ln_final`` (fold-LN convention) stay
7non-fireable.
9Names are TransformerBridge-native (``blocks.{i}.hook_out``, ``.attn.hook_out``, ...),
10not the HookedTransformer aliases. A bridge cache carries both with identical values,
11so parity vs ``boot_transformers`` still resolves.
13Which boundaries are actually fireable is decided per-model by the provider's structural
14self-check, not a hand-kept architecture list: it locates attn/mlp and probes whether the
15``resid_pre + attn_out`` derivation holds, gating ``resid_mid`` otherwise; head-split
16kinds need separate q/k/v projections (q/k/v), a locatable out-projection (z), or eager
17attention (pattern). ``supported_hook_points(n_layers, kinds=...)`` filters to that set.
18"""
19from __future__ import annotations
21import re
22from typing import Iterable, Optional
24ALL_KINDS = frozenset({"resid_pre", "resid_mid", "resid_post", "attn_out", "mlp_out"})
25# Head-split attention kinds, served only when structurally detected (never by default):
26# q/k/v are the pre-RoPE projection outputs, z is the out-projection input (all
27# ``(seq, heads, d_head)``); pattern is post-softmax attention ``(heads, q_pos, k_pos)``.
28HEAD_KINDS = frozenset({"q", "k", "v", "z", "pattern"})
30# One canonical TransformerBridge name per boundary (no aliases — avoids duplicate
31# HookPoints/cache entries). resid_mid (ln2.hook_in) is derived (resid_pre +
32# attn_out), so it's capture-only.
33_KIND_NAMES = {
34 "resid_pre": "blocks.{i}.hook_in",
35 "resid_mid": "blocks.{i}.ln2.hook_in",
36 "resid_post": "blocks.{i}.hook_out",
37 "attn_out": "blocks.{i}.attn.hook_out",
38 "mlp_out": "blocks.{i}.mlp.hook_out",
39 "q": "blocks.{i}.attn.hook_q",
40 "k": "blocks.{i}.attn.hook_k",
41 "v": "blocks.{i}.attn.hook_v",
42 "z": "blocks.{i}.attn.hook_z",
43 "pattern": "blocks.{i}.attn.hook_pattern",
44}
45# pattern is read from the forward's output_attentions — nothing to write back, so it's
46# capture-only (like the derived resid_mid).
47INTERVENEABLE_KINDS = frozenset(
48 {"resid_pre", "attn_out", "mlp_out", "resid_post", "q", "k", "v", "z"}
49)
51# Rank of each kind's batchless wire array; the driver unsqueezes exactly one batch dim.
52WIRE_BATCHLESS_NDIM = {
53 **{kind: 2 for kind in ALL_KINDS}, # (seq, d_model)
54 "q": 3,
55 "k": 3,
56 "v": 3,
57 "z": 3, # (seq, heads, d_head)
58 "pattern": 3, # (heads, q_pos, k_pos)
59}
61_SUFFIX_TO_KIND = {
62 "hook_in": "resid_pre",
63 "ln2.hook_in": "resid_mid",
64 "hook_out": "resid_post",
65 "attn.hook_out": "attn_out",
66 "mlp.hook_out": "mlp_out",
67 "attn.hook_q": "q",
68 "attn.hook_k": "k",
69 "attn.hook_v": "v",
70 "attn.hook_z": "z",
71 "attn.hook_pattern": "pattern",
72}
73_BLOCK = re.compile(r"^blocks\.(\d+)\.(.+)$")
76def supported_hook_points(n_layers: int, kinds: Optional[Iterable[str]] = None) -> frozenset[str]:
77 """Fireable hook names across all layers. ``kinds=None`` means all *boundary* kinds
78 (head-split kinds are opt-in — a provider must detect and list them explicitly);
79 pass the provider's detected kinds to gate (e.g. drop ``resid_mid`` for parallel)."""
80 selected = ALL_KINDS if kinds is None else kinds
81 return frozenset(_KIND_NAMES[k].format(i=i) for i in range(n_layers) for k in selected)
84def all_hook_points(n_layers: int) -> frozenset[str]:
85 """Every hook name the registry can serve (boundaries + head-split) — the universe a
86 driver subtracts its supported set from to build ``non_fireable_hook_points``."""
87 return supported_hook_points(n_layers, ALL_KINDS | HEAD_KINDS)
90def nonfireable_hook_points(n_layers: int) -> frozenset[str]:
91 """Hooks no provider configuration can fire (embed, ln_final, pre-softmax scores).
92 Head-split q/k/v/z/pattern are *conditionally* fireable and belong here only when a
93 model's structural probe gates them — the driver handles that subtraction."""
94 names = ["embed.hook_out", "ln_final.hook_normalized", "unembed.hook_out"]
95 names += [f"blocks.{i}.attn.hook_attn_scores" for i in range(n_layers)]
96 return frozenset(names)
99def resolve(name: str) -> tuple[int, str] | None:
100 """Canonical hook name → ``(layer, kind)``, or ``None`` if not a fireable hook."""
101 match = _BLOCK.match(name)
102 if match is None:
103 return None
104 kind = _SUFFIX_TO_KIND.get(match.group(2))
105 return (int(match.group(1)), kind) if kind is not None else None
108def wire_key(layer: int, kind: str) -> str:
109 """Stable key for one captured boundary in the activation payload."""
110 return f"{layer}:{kind}"
113def name_from_wire_key(key: str) -> str | None:
114 """Inverse of ``wire_key`` ∘ ``resolve``: ``"<layer>:<kind>"`` → the canonical hook
115 name, or ``None`` if the kind is unknown."""
116 layer, _, kind = key.partition(":")
117 template = _KIND_NAMES.get(kind)
118 return template.format(i=int(layer)) if template and layer.isdigit() else None