open-interpretability 0.0.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. open_interpretability-0.0.20.dist-info/METADATA +157 -0
  2. open_interpretability-0.0.20.dist-info/RECORD +256 -0
  3. open_interpretability-0.0.20.dist-info/WHEEL +4 -0
  4. open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
  5. open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
  6. openinterp/__init__.py +6 -0
  7. openinterp/agents/__init__.py +25 -0
  8. openinterp/agents/base.py +229 -0
  9. openinterp/agents/judge.py +149 -0
  10. openinterp/agents/proposer.py +471 -0
  11. openinterp/agents/proposer_profiles.py +79 -0
  12. openinterp/agents/review_measurements.py +173 -0
  13. openinterp/agents/reviewer.py +498 -0
  14. openinterp/agreement.py +63 -0
  15. openinterp/audit_options.py +79 -0
  16. openinterp/baseline.py +254 -0
  17. openinterp/benchmark.py +450 -0
  18. openinterp/blends.py +239 -0
  19. openinterp/builder/__init__.py +6 -0
  20. openinterp/builder/agents.py +260 -0
  21. openinterp/builder/aligned.py +41 -0
  22. openinterp/builder/charts.py +411 -0
  23. openinterp/builder/circuit_pages.py +37 -0
  24. openinterp/builder/circuits.py +264 -0
  25. openinterp/builder/compiler.py +495 -0
  26. openinterp/builder/corpora.py +98 -0
  27. openinterp/builder/coverage.py +46 -0
  28. openinterp/builder/dashboard.py +255 -0
  29. openinterp/builder/disk_memo.py +65 -0
  30. openinterp/builder/experiment.py +105 -0
  31. openinterp/builder/heat.py +121 -0
  32. openinterp/builder/hrefs.py +71 -0
  33. openinterp/builder/learned_baseline.py +204 -0
  34. openinterp/builder/model_facts.py +138 -0
  35. openinterp/builder/neuronpedia_export.py +107 -0
  36. openinterp/builder/parallel.py +100 -0
  37. openinterp/builder/parquet_export.py +458 -0
  38. openinterp/builder/pieces.py +127 -0
  39. openinterp/builder/pii.py +211 -0
  40. openinterp/builder/registry_stats.py +144 -0
  41. openinterp/builder/research.py +109 -0
  42. openinterp/builder/sections.py +28 -0
  43. openinterp/builder/significance.py +103 -0
  44. openinterp/builder/sister_graph.py +180 -0
  45. openinterp/builder/standing.py +185 -0
  46. openinterp/builder/statuses.py +34 -0
  47. openinterp/builder/stopped.py +91 -0
  48. openinterp/builder/stops.py +30 -0
  49. openinterp/builder/superpositions.py +102 -0
  50. openinterp/builder/templates/_agent_role_cells.html +9 -0
  51. openinterp/builder/templates/_causal_chart.html +67 -0
  52. openinterp/builder/templates/_circuit.html +8 -0
  53. openinterp/builder/templates/_dashboard.html +133 -0
  54. openinterp/builder/templates/_experiment.html +500 -0
  55. openinterp/builder/templates/_macros.html +82 -0
  56. openinterp/builder/templates/_patchscope.html +33 -0
  57. openinterp/builder/templates/_prior_art.html +24 -0
  58. openinterp/builder/templates/_specificity.html +21 -0
  59. openinterp/builder/templates/_steering.html +34 -0
  60. openinterp/builder/templates/_superposition.html +62 -0
  61. openinterp/builder/templates/_vocab_chart.html +88 -0
  62. openinterp/builder/templates/_wiring.html +44 -0
  63. openinterp/builder/templates/about.html +95 -0
  64. openinterp/builder/templates/agent.html +69 -0
  65. openinterp/builder/templates/agents.html +80 -0
  66. openinterp/builder/templates/base.html +103 -0
  67. openinterp/builder/templates/circuit.html +64 -0
  68. openinterp/builder/templates/circuits.html +46 -0
  69. openinterp/builder/templates/experiment.html +21 -0
  70. openinterp/builder/templates/hub.html +16 -0
  71. openinterp/builder/templates/index.html +387 -0
  72. openinterp/builder/templates/layer.html +69 -0
  73. openinterp/builder/templates/learned-baseline.html +44 -0
  74. openinterp/builder/templates/model.html +151 -0
  75. openinterp/builder/templates/models.html +47 -0
  76. openinterp/builder/templates/neuron.html +234 -0
  77. openinterp/builder/templates/paper.html +39 -0
  78. openinterp/builder/templates/redirect.html +1 -0
  79. openinterp/builder/templates/research.html +31 -0
  80. openinterp/builder/templates/user.html +54 -0
  81. openinterp/builder/templates/users.html +39 -0
  82. openinterp/builder/templates/wiki/experiment.md +271 -0
  83. openinterp/builder/templates/wiki/index.md +32 -0
  84. openinterp/builder/templates/wiki/neuron.md +30 -0
  85. openinterp/builder/templates/wiki/stop.md +36 -0
  86. openinterp/builder/tree.py +470 -0
  87. openinterp/builder/users.py +142 -0
  88. openinterp/builder/wiki.py +427 -0
  89. openinterp/checks.py +429 -0
  90. openinterp/cli.py +494 -0
  91. openinterp/cli_baseline.py +332 -0
  92. openinterp/cli_benchmark.py +300 -0
  93. openinterp/cli_build.py +128 -0
  94. openinterp/cli_check.py +51 -0
  95. openinterp/cli_concepts.py +76 -0
  96. openinterp/cli_corpus.py +252 -0
  97. openinterp/cli_costs.py +257 -0
  98. openinterp/cli_coverage.py +76 -0
  99. openinterp/cli_crossref.py +86 -0
  100. openinterp/cli_errata.py +142 -0
  101. openinterp/cli_evidence.py +495 -0
  102. openinterp/cli_evidence_store.py +93 -0
  103. openinterp/cli_explain.py +101 -0
  104. openinterp/cli_loop.py +125 -0
  105. openinterp/cli_memory.py +28 -0
  106. openinterp/cli_neuronpedia.py +175 -0
  107. openinterp/cli_pause.py +45 -0
  108. openinterp/cli_pole_effects.py +133 -0
  109. openinterp/cli_probing.py +119 -0
  110. openinterp/cli_registry.py +348 -0
  111. openinterp/cli_reproduce.py +446 -0
  112. openinterp/cli_review.py +302 -0
  113. openinterp/cli_scan.py +223 -0
  114. openinterp/cli_score.py +446 -0
  115. openinterp/cli_sisters.py +86 -0
  116. openinterp/cli_submission.py +217 -0
  117. openinterp/cli_superpositions.py +118 -0
  118. openinterp/cli_wiring.py +54 -0
  119. openinterp/codeid.py +73 -0
  120. openinterp/concepts.py +400 -0
  121. openinterp/core/__init__.py +59 -0
  122. openinterp/core/agent_names.py +76 -0
  123. openinterp/core/attribution.py +101 -0
  124. openinterp/core/cache_dirs.py +15 -0
  125. openinterp/core/causal.py +82 -0
  126. openinterp/core/confounds.py +103 -0
  127. openinterp/core/contributors.py +26 -0
  128. openinterp/core/corpora.py +126 -0
  129. openinterp/core/costs.py +430 -0
  130. openinterp/core/draws.py +109 -0
  131. openinterp/core/errata.py +244 -0
  132. openinterp/core/evidence.py +160 -0
  133. openinterp/core/examples.py +93 -0
  134. openinterp/core/exceptions.py +29 -0
  135. openinterp/core/explanation.py +130 -0
  136. openinterp/core/fence.py +42 -0
  137. openinterp/core/heavy_lock.py +114 -0
  138. openinterp/core/identifiers.py +198 -0
  139. openinterp/core/isolation.py +149 -0
  140. openinterp/core/ledger.py +130 -0
  141. openinterp/core/licenses.py +28 -0
  142. openinterp/core/manifold.py +117 -0
  143. openinterp/core/partners.py +32 -0
  144. openinterp/core/patchscope.py +59 -0
  145. openinterp/core/paths.py +40 -0
  146. openinterp/core/pause.py +96 -0
  147. openinterp/core/pricing.py +178 -0
  148. openinterp/core/prior_art.py +44 -0
  149. openinterp/core/probing.py +146 -0
  150. openinterp/core/profiles.py +221 -0
  151. openinterp/core/project.py +28 -0
  152. openinterp/core/run_environment.py +38 -0
  153. openinterp/core/schemas.py +484 -0
  154. openinterp/core/specificity.py +49 -0
  155. openinterp/core/stats.py +172 -0
  156. openinterp/core/steering.py +49 -0
  157. openinterp/core/strict.py +19 -0
  158. openinterp/core/takedowns.py +136 -0
  159. openinterp/core/transcripts.py +130 -0
  160. openinterp/core/wiring.py +80 -0
  161. openinterp/corpus.py +476 -0
  162. openinterp/corpus_cache.py +145 -0
  163. openinterp/corpus_layouts.py +108 -0
  164. openinterp/corpus_presets.py +168 -0
  165. openinterp/corpus_rebuild.py +93 -0
  166. openinterp/coverage.py +155 -0
  167. openinterp/crossref.py +442 -0
  168. openinterp/data/__init__.py +9 -0
  169. openinterp/data/offensive_words.txt +58 -0
  170. openinterp/data/public_suffix_list.dat +15771 -0
  171. openinterp/data/sample_corpus.txt +40 -0
  172. openinterp/draw_steps.py +359 -0
  173. openinterp/errata_checks.py +129 -0
  174. openinterp/evidence.py +462 -0
  175. openinterp/evidence_branch.py +119 -0
  176. openinterp/evidence_lenses.py +75 -0
  177. openinterp/experiment_loop.py +357 -0
  178. openinterp/explain.py +252 -0
  179. openinterp/half_precision.py +79 -0
  180. openinterp/harness/__init__.py +37 -0
  181. openinterp/harness/ane/__init__.py +9 -0
  182. openinterp/harness/ane/backend.py +324 -0
  183. openinterp/harness/ane/cache.py +276 -0
  184. openinterp/harness/ane/chunks.py +163 -0
  185. openinterp/harness/ane/convert.py +116 -0
  186. openinterp/harness/ane/forward.py +258 -0
  187. openinterp/harness/ane/qualified.py +40 -0
  188. openinterp/harness/annotations.py +329 -0
  189. openinterp/harness/attribution.py +96 -0
  190. openinterp/harness/causal.py +264 -0
  191. openinterp/harness/confounds.py +184 -0
  192. openinterp/harness/covariance.py +301 -0
  193. openinterp/harness/crossref.py +195 -0
  194. openinterp/harness/cuda_devices.py +88 -0
  195. openinterp/harness/dead.py +124 -0
  196. openinterp/harness/devices.py +401 -0
  197. openinterp/harness/dictionaries.py +496 -0
  198. openinterp/harness/dreaming.py +116 -0
  199. openinterp/harness/environment.py +64 -0
  200. openinterp/harness/explanation_scores.py +371 -0
  201. openinterp/harness/features.py +276 -0
  202. openinterp/harness/first_position.py +137 -0
  203. openinterp/harness/heads.py +226 -0
  204. openinterp/harness/hooks.py +499 -0
  205. openinterp/harness/input_attribution.py +367 -0
  206. openinterp/harness/jlens.py +183 -0
  207. openinterp/harness/layer_cache.py +301 -0
  208. openinterp/harness/layer_major.py +274 -0
  209. openinterp/harness/lookahead.py +93 -0
  210. openinterp/harness/manifold.py +200 -0
  211. openinterp/harness/memory.py +471 -0
  212. openinterp/harness/moe.py +134 -0
  213. openinterp/harness/patcher.py +322 -0
  214. openinterp/harness/patchscope.py +177 -0
  215. openinterp/harness/probing.py +383 -0
  216. openinterp/harness/scan.py +462 -0
  217. openinterp/harness/scan_backend.py +201 -0
  218. openinterp/harness/sister_passes.py +463 -0
  219. openinterp/harness/sisters.py +363 -0
  220. openinterp/harness/sites.py +101 -0
  221. openinterp/harness/specificity.py +107 -0
  222. openinterp/harness/steering.py +159 -0
  223. openinterp/harness/superpositions.py +116 -0
  224. openinterp/harness/units.py +203 -0
  225. openinterp/harness/wiring.py +432 -0
  226. openinterp/harness/wiring_activations.py +285 -0
  227. openinterp/langid.py +73 -0
  228. openinterp/names.py +129 -0
  229. openinterp/neuronpedia_baseline.py +88 -0
  230. openinterp/neuronpedia_review.py +148 -0
  231. openinterp/planner.py +466 -0
  232. openinterp/planner_effects.py +89 -0
  233. openinterp/planner_heads.py +92 -0
  234. openinterp/planner_models.py +174 -0
  235. openinterp/planner_sisters.py +122 -0
  236. openinterp/planner_states.py +57 -0
  237. openinterp/planner_superpositions.py +114 -0
  238. openinterp/pole_effects.py +102 -0
  239. openinterp/prior_art.py +143 -0
  240. openinterp/reproduce.py +450 -0
  241. openinterp/revision.py +45 -0
  242. openinterp/runner_cache.py +35 -0
  243. openinterp/score_controls.py +169 -0
  244. openinterp/sensitive.py +243 -0
  245. openinterp/show.py +236 -0
  246. openinterp/signing.py +209 -0
  247. openinterp/sisters.py +476 -0
  248. openinterp/superposition_finders.py +225 -0
  249. openinterp/sync/__init__.py +5 -0
  250. openinterp/sync/r2.py +280 -0
  251. openinterp/syntax.py +399 -0
  252. openinterp/takedown_checks.py +76 -0
  253. openinterp/toxicity.py +158 -0
  254. openinterp/verdict_regression.py +79 -0
  255. openinterp/wiring_backfill.py +170 -0
  256. openinterp/wordnet.py +362 -0
@@ -0,0 +1,432 @@
1
+ """A unit's top upstream and downstream connections, from weights and from activations.
2
+
3
+ The weights reading is virtual weights (Elhage et al., 2021). The activations reading is
4
+ attribution patching on prompts where the unit fires (Syed et al., 2023).
5
+ :class:`~openinterp.core.wiring.Wiring` defines each number.
6
+
7
+ Every unit is described the same way, by where it reads the residual stream and what it
8
+ writes back (:class:`Target`):
9
+
10
+ * a neuron pole reads its MLP's input (``gate``/``up``, or ``in``) and writes ``w_out``;
11
+ * a dictionary feature reads its dictionary's input site with its encoder row. It writes
12
+ its decoder row into the MLP's write to the residual stream. (A residual-stream SAE's
13
+ latent reads and writes the residual stream after its layer.)
14
+ * an attention head reads its layer's attention input through ``q``, ``k`` and ``v`` and
15
+ writes a subspace, ``W_O``.
16
+
17
+ Both readings are direct: they cover the residual-stream path from one unit's write to
18
+ another's read, not paths through units in between.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Callable, Sequence
24
+ from contextlib import nullcontext
25
+ from dataclasses import dataclass
26
+ from typing import Literal
27
+
28
+ import torch
29
+ from torch import nn
30
+
31
+ from openinterp.core.exceptions import ArchitectureNotSupportedError
32
+ from openinterp.core.wiring import Wiring, WiringEdge
33
+ from openinterp.harness.hooks import (
34
+ PRE_MLP_NORM_PATHS,
35
+ NeuronHookRunner,
36
+ _try_path,
37
+ isolate_pole,
38
+ pole_strength,
39
+ )
40
+ from openinterp.harness.memory import loaded
41
+ from openinterp.harness.sites import normalize_without_affine
42
+ from openinterp.harness.units import FeatureUnit, HeadUnit, NeuronUnit, Unit
43
+
44
+ # A neuron's reads of its MLP input, by channel; the first set that resolves wins.
45
+ MLP_READ_PATHS = (
46
+ {"gate": "mlp.gate_proj", "up": "mlp.up_proj"}, # Llama, Qwen2, Mistral, Gemma
47
+ {"in": "mlp.c_fc"}, # GPT-2
48
+ {"in": "mlp.dense_h_to_4h"}, # GPT-NeoX, Falcon
49
+ {"in": "mlp.fc_in"}, # GPT-J
50
+ {"in": "mlp.fc1"}, # Phi
51
+ {"in": "fc1"}, # OPT
52
+ {"in": "mlp.up_proj"}, # an ungated MLP named like Llama's
53
+ )
54
+ # Separate query/key/value projections; GPT-2 fuses them in ``attn.c_attn``.
55
+ QKV_PATHS = ("self_attn.q_proj", "self_attn.k_proj", "self_attn.v_proj")
56
+ FUSED_QKV_PATH = "attn.c_attn"
57
+ PRE_ATTN_NORM_PATHS = (
58
+ "input_layernorm", # Llama, Qwen2, Mistral, Gemma, GPT-NeoX
59
+ "ln_1", # GPT-2, GPT-J
60
+ "self_attn_layer_norm", # OPT
61
+ )
62
+ QKV = ("q", "k", "v")
63
+
64
+ Site = Literal["attn_in", "mlp_in", "mlp_out", "resid_post"]
65
+
66
+
67
+ def _matrix(proj: nn.Module) -> torch.Tensor:
68
+ """A projection's weight as ``[out, in]`` (transformers' Conv1D stores ``[in, out]``)."""
69
+ with loaded(proj): # streamed: the weights are real only inside this block
70
+ weight = proj.weight.detach().float()
71
+ return weight if hasattr(proj, "in_features") else weight.T
72
+
73
+
74
+ def _project(proj: nn.Module, x: torch.Tensor) -> torch.Tensor:
75
+ """``proj(x)`` without running its hooks."""
76
+ with loaded(proj):
77
+ weight = proj.weight if hasattr(proj, "in_features") else proj.weight.T
78
+ return torch.nn.functional.linear(x, weight, proj.bias)
79
+
80
+
81
+ def _norm(block: nn.Module, paths: Sequence[str]) -> nn.Module | None:
82
+ for path in paths:
83
+ module = _try_path(block, path)
84
+ if isinstance(module, nn.Module):
85
+ return module
86
+ return None
87
+
88
+
89
+ def _fold(norm: nn.Module | None, rows: torch.Tensor, *, gain: bool = True) -> torch.Tensor:
90
+ """Take read vectors ``[..., d]`` through ``norm`` back to the residual stream.
91
+
92
+ That applies the gain (unless ``gain`` is False), then centring for LayerNorm. The
93
+ per-token scale (1/σ) is dropped.
94
+ """
95
+ rows = rows.float()
96
+ with loaded(norm) if norm is not None else nullcontext():
97
+ weight = getattr(norm, "weight", None) if gain else None
98
+ if weight is not None:
99
+ rows = rows * weight.detach().float().to(rows.device)
100
+ if isinstance(norm, nn.LayerNorm):
101
+ rows = rows - rows.mean(-1, keepdim=True)
102
+ return rows
103
+
104
+
105
+ def mlp_reads(runner: NeuronHookRunner, layer: int) -> dict[str, torch.Tensor]:
106
+ """``{channel: [width, d_model]}``, each neuron's read of the MLP input, norm folded in."""
107
+ block = runner.layers[layer]
108
+ for paths in MLP_READ_PATHS:
109
+ modules = {c: _try_path(block, p) for c, p in paths.items()}
110
+ if all(isinstance(m, nn.Module) for m in modules.values()):
111
+ norm = _norm(block, PRE_MLP_NORM_PATHS)
112
+ return {c: _fold(norm, _matrix(m)) for c, m in modules.items()}
113
+ raise ArchitectureNotSupportedError(
114
+ f"Could not find the MLP input projection under {runner.layer_path}[{layer}]"
115
+ )
116
+
117
+
118
+ def _qkv_modules(block: nn.Module) -> dict[str, tuple[nn.Module, slice | None]] | None:
119
+ """Each of q/k/v's projection, with its slice of the output when they are fused."""
120
+ fused = _try_path(block, FUSED_QKV_PATH)
121
+ if isinstance(fused, nn.Module):
122
+ width = _matrix(fused).shape[0] // 3
123
+ return {c: (fused, slice(i * width, (i + 1) * width)) for i, c in enumerate(QKV)}
124
+ modules = [_try_path(block, p) for p in QKV_PATHS]
125
+ if all(isinstance(m, nn.Module) for m in modules):
126
+ return {c: (m, None) for c, m in zip(QKV, modules, strict=True)}
127
+ return None
128
+
129
+
130
+ def attention_reads(runner: NeuronHookRunner, layer: int) -> dict[str, torch.Tensor] | None:
131
+ """Return ``{"q"|"k"|"v": [n_heads, head_dim, d_model]}``, with the input norm folded in.
132
+
133
+ Keys and values are repeated across the query heads that share them. ``None`` when
134
+ the architecture's attention isn't supported.
135
+ """
136
+ block = runner.layers[layer]
137
+ try:
138
+ n, hd = runner.n_heads, runner.head_dim(layer)
139
+ except ArchitectureNotSupportedError:
140
+ return None
141
+ modules = _qkv_modules(block)
142
+ if modules is None:
143
+ return None
144
+ norm = _norm(block, PRE_ATTN_NORM_PATHS)
145
+ reads = {}
146
+ for c, (module, part) in modules.items():
147
+ m = _matrix(module)
148
+ heads = (m if part is None else m[part]).reshape(-1, hd, m.shape[-1])
149
+ reads[c] = _fold(norm, heads.repeat_interleave(n // heads.shape[0], dim=0))
150
+ return reads
151
+
152
+
153
+ def _head_outputs(runner: NeuronHookRunner, layer: int) -> torch.Tensor:
154
+ """W_O per head: ``[n_heads, d_model, head_dim]``."""
155
+ w_o = _matrix(runner.attn_out(layer))
156
+ return w_o.reshape(w_o.shape[0], runner.n_heads, -1).permute(1, 0, 2)
157
+
158
+
159
+ def _has_attention(runner: NeuronHookRunner) -> bool:
160
+ try:
161
+ runner.attn_out(0)
162
+ except ArchitectureNotSupportedError:
163
+ return False
164
+ return attention_reads(runner, 0) is not None
165
+
166
+
167
+ # -- what a unit reads and writes ---------------------------------------------------------
168
+
169
+
170
+ @dataclass
171
+ class Target:
172
+ """Where a unit reads and writes, in the terms both readings need.
173
+
174
+ ``reads`` are residual-space read matrices ``[m, d]`` by channel (``m = 1`` for a
175
+ vector). ``write`` is ``[d]`` (a direction) or ``[d, h]`` (a head's subspace). It reads
176
+ at ``site`` of ``layer``. It writes after that layer's attention (heads) or MLP.
177
+ ``strength`` maps the residual stream at its read point ``[b, seq, d]`` to strength
178
+ ``[b, seq]``. It is ``None`` for a head, which runs the model's attention instead.
179
+ ``written`` maps the clean pass's captures to its write ``[b, seq, d]``.
180
+ """
181
+
182
+ layer: int
183
+ site: Site
184
+ reads: dict[str, torch.Tensor]
185
+ write: torch.Tensor
186
+ strength: Callable[[torch.Tensor], torch.Tensor] | None
187
+ written: Callable[[dict], torch.Tensor]
188
+ head: int | None = None
189
+ mean_output: torch.Tensor | None = None
190
+
191
+ @property
192
+ def writes_after_attention(self) -> bool:
193
+ """True for a head, which writes after its layer's attention, not its MLP."""
194
+ return self.site == "attn_in"
195
+
196
+
197
+ def target_of(runner: NeuronHookRunner, unit: Unit) -> Target:
198
+ """Where ``unit`` reads and writes; raises TypeError for an unknown kind of unit."""
199
+ if isinstance(unit, NeuronUnit):
200
+ return _neuron_target(runner, unit)
201
+ if isinstance(unit, FeatureUnit):
202
+ return _feature_target(runner, unit)
203
+ if isinstance(unit, HeadUnit):
204
+ return _head_target(runner, unit)
205
+ raise TypeError(f"no wiring for {type(unit).__name__}")
206
+
207
+
208
+ def _neuron_target(runner: NeuronHookRunner, unit: NeuronUnit) -> Target:
209
+ layer, idx, pole = unit.ident.layer, unit.ident.neuron_idx, unit.ident.pole
210
+ reads = {c: r[idx][None] for c, r in mlp_reads(runner, layer).items()}
211
+ w_out = runner.neuron_output_direction(layer, idx).float()
212
+ return Target(
213
+ layer,
214
+ "mlp_in",
215
+ reads,
216
+ -w_out if pole == "negative" else w_out,
217
+ strength=lambda x: pole_strength(_mlp_activations(runner, layer, x)[..., idx], pole),
218
+ written=lambda cap: (
219
+ isolate_pole(cap["acts"][layer][..., idx], pole)[..., None]
220
+ * w_out.to(cap["acts"][layer].device)
221
+ ),
222
+ )
223
+
224
+
225
+ def _feature_target(runner: NeuronHookRunner, unit: FeatureUnit) -> Target:
226
+ layer = unit.ident.layer
227
+ dictionary, f = unit.dictionary(runner), unit.ident.feature_idx
228
+ site = unit.profile.input_site
229
+ if site == "mlp_hidden":
230
+ raise ArchitectureNotSupportedError("wiring doesn't read a basis of the MLP's hidden space")
231
+ norm = runner.mlp_norm(layer) if site in ("mlp_in", "mlp_in_normalized") else None
232
+ read = dictionary.w_enc[:, f].float()[None]
233
+ if norm is not None: # back through the norm, without its gain if normalised
234
+ read = _fold(norm, read, gain=site == "mlp_in")
235
+
236
+ def feature(x: torch.Tensor) -> torch.Tensor:
237
+ if site == "mlp_in": # the norm itself, in the model's dtype
238
+ x = norm(x.to(norm.weight.dtype)).float()
239
+ elif norm is not None:
240
+ x = normalize_without_affine(norm, x)
241
+ return dictionary.feature(x, f)
242
+
243
+ w_dec = dictionary.w_dec[f].float()
244
+ return Target(
245
+ layer,
246
+ site if site in ("mlp_out", "resid_post") else "mlp_in",
247
+ {"in": read},
248
+ w_dec,
249
+ strength=feature,
250
+ written=lambda cap: feature(cap["read"])[..., None] * w_dec.to(cap["read"].device),
251
+ )
252
+
253
+
254
+ def _head_target(runner: NeuronHookRunner, unit: HeadUnit) -> Target:
255
+ layer = unit.ident.layer
256
+ reads = attention_reads(runner, layer)
257
+ if reads is None:
258
+ raise ArchitectureNotSupportedError("this model's attention reads aren't supported")
259
+ h = unit.ident.head_idx
260
+ w_o = runner.head_output_weights(layer, h).float() # [d, hd]
261
+ mean = torch.tensor(unit.mean_output, dtype=torch.float32)
262
+
263
+ def head_write(cap):
264
+ z = cap["z"][layer]
265
+ z = z.reshape(*z.shape[:-1], runner.n_heads, -1)[..., h, :]
266
+ return (z - mean.to(z.device)) @ w_o.T.to(z.device)
267
+
268
+ return Target(
269
+ layer,
270
+ "attn_in",
271
+ {c: r[h] for c, r in reads.items()},
272
+ w_o,
273
+ strength=None,
274
+ written=head_write,
275
+ head=h,
276
+ mean_output=mean,
277
+ )
278
+
279
+
280
+ def _upstream_layers(t: Target) -> tuple[range, range]:
281
+ """Layers of the neurons and heads that write before ``t`` reads."""
282
+ if t.site == "mlp_out":
283
+ return range(t.layer, t.layer + 1), range(0)
284
+ if t.site == "resid_post":
285
+ return range(t.layer + 1), range(t.layer + 1)
286
+ if t.site == "attn_in":
287
+ return range(t.layer), range(t.layer)
288
+ return range(t.layer), range(t.layer + 1)
289
+
290
+
291
+ def _downstream_layers(runner: NeuronHookRunner, t: Target) -> tuple[range, range]:
292
+ """Layers of the neurons and heads that read after ``t`` writes."""
293
+ first = t.layer if t.writes_after_attention else t.layer + 1
294
+ return range(first, runner.n_layers), range(t.layer + 1, runner.n_layers)
295
+
296
+
297
+ # -- ranking --------------------------------------------------------------------------------
298
+
299
+
300
+ def _top(
301
+ kind: str, scores: Sequence[tuple[int, torch.Tensor]], channels: Sequence[str], k: int
302
+ ) -> list[WiringEdge]:
303
+ """The ``k`` units of largest |weight| from ``(layer, [units, channels])`` blocks."""
304
+ if not scores:
305
+ return []
306
+ layers = torch.cat([torch.full((s.shape[0],), layer) for layer, s in scores])
307
+ units = torch.cat([torch.arange(s.shape[0]) for _, s in scores])
308
+ values = torch.cat([s.float().cpu() for _, s in scores])
309
+ pick = values.abs().argmax(-1, keepdim=True)
310
+ weight = values.gather(-1, pick).squeeze(-1)
311
+ order = weight.abs().argsort(descending=True, stable=True)[:k]
312
+ named = len(channels) > 1 or channels[0] != ""
313
+ return [
314
+ WiringEdge(
315
+ kind=kind,
316
+ layer=int(layers[i]),
317
+ index=int(units[i]),
318
+ weight=round(float(weight[i]), 6),
319
+ channels=(
320
+ {c: round(float(v), 6) for c, v in zip(channels, values[i], strict=True)}
321
+ if named
322
+ else {}
323
+ ),
324
+ )
325
+ for i in order.tolist()
326
+ ]
327
+
328
+
329
+ # -- from the weights -----------------------------------------------------------------------
330
+
331
+
332
+ def _read_vectors(read: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
333
+ """Return how much a read ``[m, d]`` picks up of each vector in ``x`` ``[n, d]``.
334
+
335
+ The result is signed for a vector read (``m = 1``), else the norm of what it reads.
336
+ """
337
+ projected = x @ read.T # [n, m]
338
+ return projected[:, 0] if read.shape[0] == 1 else projected.norm(dim=-1)
339
+
340
+
341
+ @torch.no_grad()
342
+ def weight_wiring(runner: NeuronHookRunner, unit: Unit, *, k: int = 10) -> Wiring:
343
+ """The unit's top connections by virtual weight; see :class:`Wiring`."""
344
+ t = target_of(runner, unit)
345
+ channels = list(t.reads)
346
+ # Unit-normalised reads, so channels with different scales compare.
347
+ reads = {c: r / r.norm().clamp(min=1e-12) for c, r in t.reads.items()}
348
+ neuron_layers, head_layers = _upstream_layers(t)
349
+ up_neurons = [
350
+ (
351
+ i,
352
+ torch.stack(
353
+ [_read_vectors(reads[c], _matrix(runner.down_proj(i)).T) for c in channels], -1
354
+ ),
355
+ )
356
+ for i in neuron_layers
357
+ ]
358
+ attention = _has_attention(runner)
359
+ up_heads = _upstream_heads(runner, reads, head_layers, k) if attention else None
360
+ neuron_layers, head_layers = _downstream_layers(runner, t)
361
+ down_neurons, down_channels = _downstream_neurons(runner, t.write, neuron_layers)
362
+ down_heads = _downstream_heads(runner, t.write, head_layers, k) if attention else None
363
+ return Wiring(
364
+ method="weights",
365
+ upstream_neurons=_top("mlp_neuron", up_neurons, channels, k),
366
+ upstream_heads=up_heads,
367
+ downstream_neurons=_top("mlp_neuron", down_neurons, down_channels, k),
368
+ downstream_heads=down_heads,
369
+ )
370
+
371
+
372
+ def _upstream_heads(runner: NeuronHookRunner, reads: dict, layers, k: int):
373
+ channels = list(reads)
374
+ found = []
375
+ for i in layers:
376
+ ov = torch.einsum("ndz,nzh->ndh", _head_outputs(runner, i), attention_reads(runner, i)["v"])
377
+ # [n, d, d]: each head's OV circuit, read by each channel.
378
+ found.append(
379
+ (i, torch.stack([(reads[c] @ ov).flatten(1).norm(dim=-1) for c in channels], -1))
380
+ )
381
+ return _top("attn_head", found, channels, k)
382
+
383
+
384
+ def _downstream_neurons(runner: NeuronHookRunner, write: torch.Tensor, layers):
385
+ """Each later layer's neurons' read of ``write``, per channel, and the channel names."""
386
+ found, channels = [], [""]
387
+ for i in layers:
388
+ later = mlp_reads(runner, i)
389
+ if write.dim() == 1:
390
+ scores = [later[c] @ write for c in later]
391
+ else:
392
+ scores = [(later[c] @ write).norm(dim=-1) for c in later]
393
+ found.append((i, torch.stack(scores, -1)))
394
+ channels = list(later)
395
+ return found, channels
396
+
397
+
398
+ def _downstream_heads(runner: NeuronHookRunner, write: torch.Tensor, layers, k: int):
399
+ found = []
400
+ for i in layers:
401
+ qkv = attention_reads(runner, i)
402
+ found.append((i, torch.stack([(qkv[c] @ write).flatten(1).norm(dim=-1) for c in QKV], -1)))
403
+ return _top("attn_head", found, list(QKV), k)
404
+
405
+
406
+ # -- from activations -----------------------------------------------------------------------
407
+
408
+
409
+ def _mlp_activations(runner: NeuronHookRunner, layer: int, x: torch.Tensor) -> torch.Tensor:
410
+ """``layer``'s neuron activations for residual-stream inputs ``x`` ``[..., d_model]``."""
411
+ norm = _norm(runner.layers[layer], PRE_MLP_NORM_PATHS)
412
+ captured: list[torch.Tensor] = []
413
+ handle = runner.down_proj(layer).register_forward_pre_hook(
414
+ lambda _m, args: captured.append(args[0])
415
+ )
416
+ try:
417
+ runner.mlp(layer)(norm(x) if norm is not None else x)
418
+ finally:
419
+ handle.remove()
420
+ return captured[-1]
421
+
422
+
423
+ def _mlp_read_point(runner: NeuronHookRunner, layer: int) -> nn.Module:
424
+ """The module whose input is the residual stream ``layer``'s MLP reads."""
425
+ return _norm(runner.layers[layer], PRE_MLP_NORM_PATHS) or runner.mlp(layer)
426
+
427
+
428
+ def _attn_read_point(runner: NeuronHookRunner, layer: int) -> nn.Module:
429
+ norm = _norm(runner.layers[layer], PRE_ATTN_NORM_PATHS)
430
+ if norm is None:
431
+ raise ArchitectureNotSupportedError("Could not find the norm before attention")
432
+ return norm