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.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- 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
|