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,285 @@
|
|
|
1
|
+
"""A unit's wiring from activations, at the peaks of prompts where it fires.
|
|
2
|
+
|
|
3
|
+
Upstream, it uses attribution patching (Syed et al., 2023). Downstream, it removes the
|
|
4
|
+
unit's write from each later unit's input. :class:`~openinterp.core.wiring.Wiring`
|
|
5
|
+
defines each number. The unit is described as in :mod:`openinterp.harness.wiring`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from contextlib import suppress
|
|
12
|
+
|
|
13
|
+
import torch
|
|
14
|
+
|
|
15
|
+
from openinterp.core.exceptions import ArchitectureNotSupportedError
|
|
16
|
+
from openinterp.core.wiring import Wiring
|
|
17
|
+
from openinterp.harness.hooks import NeuronHookRunner, _StopForward
|
|
18
|
+
from openinterp.harness.units import Unit
|
|
19
|
+
from openinterp.harness.wiring import (
|
|
20
|
+
QKV,
|
|
21
|
+
Target,
|
|
22
|
+
_attn_read_point,
|
|
23
|
+
_downstream_layers,
|
|
24
|
+
_has_attention,
|
|
25
|
+
_head_outputs,
|
|
26
|
+
_matrix,
|
|
27
|
+
_mlp_activations,
|
|
28
|
+
_mlp_read_point,
|
|
29
|
+
_project,
|
|
30
|
+
_qkv_modules,
|
|
31
|
+
_top,
|
|
32
|
+
_upstream_layers,
|
|
33
|
+
target_of,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _clean_pass(runner, t: Target, encoded, attention: bool) -> dict:
|
|
38
|
+
"""One forward pass keeping what both directions need, all float32 ``[b, seq, ...]``.
|
|
39
|
+
|
|
40
|
+
The pass stops after the deepest layer it reads, so it computes no logits.
|
|
41
|
+
"""
|
|
42
|
+
up_n, up_h = _upstream_layers(t)
|
|
43
|
+
down_n, down_h = _downstream_layers(runner, t)
|
|
44
|
+
cap = {"acts": {}, "z": {}, "mlp_in": {}, "attn_in": {}}
|
|
45
|
+
|
|
46
|
+
def keep(store, i, out=False):
|
|
47
|
+
if out: # a decoder layer may return a tuple whose first item is the residual
|
|
48
|
+
return lambda _m, _a, o: store.__setitem__(
|
|
49
|
+
i, (o[0] if isinstance(o, tuple) else o).detach().float()
|
|
50
|
+
)
|
|
51
|
+
return lambda _m, args: store.__setitem__(i, args[0].detach().float())
|
|
52
|
+
|
|
53
|
+
hooks = [(runner.down_proj(i), keep(cap["acts"], i)) for i in {*up_n, t.layer}]
|
|
54
|
+
hooks += [(_mlp_read_point(runner, i), keep(cap["mlp_in"], i)) for i in {*down_n, t.layer}]
|
|
55
|
+
if attention:
|
|
56
|
+
hooks += [(runner.attn_out(i), keep(cap["z"], i)) for i in {*up_h, *down_h, t.layer}]
|
|
57
|
+
hooks += [
|
|
58
|
+
(_attn_read_point(runner, i), keep(cap["attn_in"], i)) for i in {*down_h, t.layer}
|
|
59
|
+
]
|
|
60
|
+
handles = [m.register_forward_pre_hook(fn) for m, fn in hooks]
|
|
61
|
+
written: dict[int, torch.Tensor] = {} # the read of a unit reading its layer's output
|
|
62
|
+
if t.site in ("mlp_out", "resid_post"):
|
|
63
|
+
module = runner.mlp_write(t.layer) if t.site == "mlp_out" else runner.layers[t.layer]
|
|
64
|
+
handles.append(module.register_forward_hook(keep(written, t.layer, out=True)))
|
|
65
|
+
deepest = max({*up_n, *up_h, *down_n, *down_h, t.layer})
|
|
66
|
+
|
|
67
|
+
def stop(*_) -> None:
|
|
68
|
+
raise _StopForward
|
|
69
|
+
|
|
70
|
+
handles.append(runner.layers[deepest].register_forward_hook(stop))
|
|
71
|
+
try:
|
|
72
|
+
with torch.no_grad(), suppress(_StopForward):
|
|
73
|
+
runner.forward(encoded)
|
|
74
|
+
finally:
|
|
75
|
+
for h in handles:
|
|
76
|
+
h.remove()
|
|
77
|
+
reads = {"mlp_in": cap["mlp_in"], "attn_in": cap["attn_in"]}
|
|
78
|
+
cap["read"] = reads.get(t.site, written)[t.layer]
|
|
79
|
+
return cap
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _head_strength(runner, t: Target, encoded, x: torch.Tensor) -> torch.Tensor:
|
|
83
|
+
"""A head's strength ``[b, seq]`` with its layer's attention input replaced by ``x``."""
|
|
84
|
+
z: list[torch.Tensor] = []
|
|
85
|
+
|
|
86
|
+
def capture(_m, args):
|
|
87
|
+
z.append(args[0])
|
|
88
|
+
raise _StopForward
|
|
89
|
+
|
|
90
|
+
handles = [
|
|
91
|
+
_attn_read_point(runner, t.layer).register_forward_pre_hook(
|
|
92
|
+
lambda _m, args: (x.to(args[0].dtype), *args[1:])
|
|
93
|
+
),
|
|
94
|
+
runner.attn_out(t.layer).register_forward_pre_hook(capture),
|
|
95
|
+
]
|
|
96
|
+
try:
|
|
97
|
+
with suppress(_StopForward):
|
|
98
|
+
runner.forward(encoded)
|
|
99
|
+
finally:
|
|
100
|
+
for h in handles:
|
|
101
|
+
h.remove()
|
|
102
|
+
out = z[-1].reshape(*z[-1].shape[:-1], runner.n_heads, -1)[..., t.head, :].float()
|
|
103
|
+
w_o = t.write.to(out.device)
|
|
104
|
+
return ((out - t.mean_output.to(out.device)) @ w_o.T).norm(dim=-1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _downstream_head_effects(runner, t: Target, encoded, cap, delta, layer, peaks) -> torch.Tensor:
|
|
108
|
+
"""Return ``[b, n_heads, 3]``: how far each head's write at the peak moves.
|
|
109
|
+
|
|
110
|
+
It moves when ``delta`` is removed from what the head's ``q``, ``k`` or ``v`` reads,
|
|
111
|
+
one channel at a time.
|
|
112
|
+
"""
|
|
113
|
+
modules = _qkv_modules(runner.layers[layer])
|
|
114
|
+
w_o = _head_outputs(runner, layer) # [n, d, hd]
|
|
115
|
+
rows = torch.arange(len(peaks))
|
|
116
|
+
clean = cap["z"][layer][rows, peaks].reshape(len(peaks), runner.n_heads, -1)
|
|
117
|
+
effects = []
|
|
118
|
+
for c in QKV:
|
|
119
|
+
module, part = modules[c]
|
|
120
|
+
z = _z_without(runner, layer, module, part, delta, encoded)
|
|
121
|
+
changed = z[rows, peaks].reshape(len(peaks), runner.n_heads, -1)
|
|
122
|
+
moved = torch.einsum("ndh,bnh->bnd", w_o.to(changed.device), clean - changed)
|
|
123
|
+
effects.append(moved.norm(dim=-1))
|
|
124
|
+
return torch.stack(effects, -1)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _z_without(runner, layer: int, module, part, delta, encoded) -> torch.Tensor:
|
|
128
|
+
"""Return the layer's head outputs (float32) when one channel reads without ``delta``.
|
|
129
|
+
|
|
130
|
+
The channel is one of ``q``, ``k`` or ``v``: ``module``, or its ``part`` of a fused
|
|
131
|
+
projection.
|
|
132
|
+
"""
|
|
133
|
+
norm = _attn_read_point(runner, layer)
|
|
134
|
+
state: dict = {}
|
|
135
|
+
|
|
136
|
+
def before(_m, args):
|
|
137
|
+
if not state.get("busy"):
|
|
138
|
+
state["busy"] = True
|
|
139
|
+
try:
|
|
140
|
+
state["normed"] = norm(args[0] - delta.to(args[0].dtype))
|
|
141
|
+
finally:
|
|
142
|
+
state["busy"] = False
|
|
143
|
+
|
|
144
|
+
def swap(_m, _args, out):
|
|
145
|
+
alt = _project(module, state["normed"])
|
|
146
|
+
if part is None:
|
|
147
|
+
return alt
|
|
148
|
+
out = out.clone()
|
|
149
|
+
out[..., part] = alt[..., part]
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
z: list[torch.Tensor] = []
|
|
153
|
+
|
|
154
|
+
def capture(_m, args):
|
|
155
|
+
z.append(args[0].float())
|
|
156
|
+
raise _StopForward
|
|
157
|
+
|
|
158
|
+
handles = [
|
|
159
|
+
norm.register_forward_pre_hook(before),
|
|
160
|
+
module.register_forward_hook(swap),
|
|
161
|
+
runner.attn_out(layer).register_forward_pre_hook(capture),
|
|
162
|
+
]
|
|
163
|
+
try:
|
|
164
|
+
with torch.no_grad(), suppress(_StopForward):
|
|
165
|
+
runner.forward(encoded)
|
|
166
|
+
finally:
|
|
167
|
+
for h in handles:
|
|
168
|
+
h.remove()
|
|
169
|
+
return z[-1]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def activation_wiring(
|
|
173
|
+
runner: NeuronHookRunner,
|
|
174
|
+
unit: Unit,
|
|
175
|
+
prompts: Sequence[str],
|
|
176
|
+
threshold: float,
|
|
177
|
+
*,
|
|
178
|
+
k: int = 10,
|
|
179
|
+
batch_size: int = 8,
|
|
180
|
+
) -> Wiring | None:
|
|
181
|
+
"""Return the unit's top connections by attribution, at its peak on each firing prompt.
|
|
182
|
+
|
|
183
|
+
A prompt fires when the unit reaches ``threshold``. ``None`` if it reaches it on none.
|
|
184
|
+
See :class:`Wiring`.
|
|
185
|
+
"""
|
|
186
|
+
t = target_of(runner, unit)
|
|
187
|
+
attention = _has_attention(runner)
|
|
188
|
+
if t.site == "attn_in" and not attention:
|
|
189
|
+
raise ArchitectureNotSupportedError("a head's wiring needs supported attention")
|
|
190
|
+
up_n, up_h = _upstream_layers(t)
|
|
191
|
+
down_n, down_h = _downstream_layers(runner, t)
|
|
192
|
+
if not attention:
|
|
193
|
+
up_h = down_h = range(0)
|
|
194
|
+
w_out = {i: _matrix(runner.down_proj(i)) for i in up_n} # [d, width]
|
|
195
|
+
w_o = {i: _head_outputs(runner, i) for i in up_h} if attention else {}
|
|
196
|
+
dtype = next(runner.model.parameters()).dtype
|
|
197
|
+
acc = _Sums()
|
|
198
|
+
for start in range(0, len(prompts), batch_size):
|
|
199
|
+
encoded = runner.encode(list(prompts[start : start + batch_size]))
|
|
200
|
+
cap = _clean_pass(runner, t, encoded, attention)
|
|
201
|
+
found = _peak_gradient(runner, t, unit, encoded, cap, dtype, threshold)
|
|
202
|
+
if found is None:
|
|
203
|
+
continue
|
|
204
|
+
grad, peaks, fired = found
|
|
205
|
+
acc.used += int(fired.sum())
|
|
206
|
+
with torch.no_grad():
|
|
207
|
+
_add_upstream(acc, runner, cap, grad, fired, up_n, up_h, w_out, w_o)
|
|
208
|
+
_add_downstream(acc, runner, t, encoded, cap, fired, peaks, down_n, down_h, dtype)
|
|
209
|
+
if not acc.used:
|
|
210
|
+
return None
|
|
211
|
+
return Wiring(
|
|
212
|
+
method="activations",
|
|
213
|
+
upstream_neurons=_top("mlp_neuron", acc.mean("up_n", up_n), [""], k),
|
|
214
|
+
upstream_heads=_top("attn_head", acc.mean("up_h", up_h), [""], k) if attention else None,
|
|
215
|
+
downstream_neurons=_top("mlp_neuron", acc.mean("down_n", down_n), [""], k),
|
|
216
|
+
downstream_heads=_top("attn_head", acc.mean("down_h", down_h), list(QKV), k)
|
|
217
|
+
if attention
|
|
218
|
+
else None,
|
|
219
|
+
prompts=acc.used,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class _Sums:
|
|
224
|
+
"""Per-(direction, layer) sums over the prompts where the unit fired, and their count."""
|
|
225
|
+
|
|
226
|
+
def __init__(self) -> None:
|
|
227
|
+
self.sums: dict[tuple[str, int], torch.Tensor] = {}
|
|
228
|
+
self.used = 0
|
|
229
|
+
|
|
230
|
+
def add(self, key, value) -> None:
|
|
231
|
+
self.sums[key] = self.sums[key] + value if key in self.sums else value
|
|
232
|
+
|
|
233
|
+
def mean(self, kind: str, layers) -> list[tuple[int, torch.Tensor]]:
|
|
234
|
+
blocks = []
|
|
235
|
+
for i in layers:
|
|
236
|
+
v = self.sums[(kind, i)] / self.used
|
|
237
|
+
blocks.append((i, v if v.dim() == 2 else v[:, None]))
|
|
238
|
+
return blocks
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _peak_gradient(runner, t: Target, unit: Unit, encoded, cap, dtype, threshold: float):
|
|
242
|
+
"""Return the read gradients, the peaks, and the mask of prompts that fired.
|
|
243
|
+
|
|
244
|
+
Each gradient is of a firing prompt's peak strength with respect to the unit's read.
|
|
245
|
+
``None`` if no prompt fired.
|
|
246
|
+
"""
|
|
247
|
+
mask = encoded.get("attention_mask", torch.ones_like(encoded["input_ids"])).bool()
|
|
248
|
+
x = cap["read"].to(dtype).requires_grad_(True)
|
|
249
|
+
with torch.enable_grad():
|
|
250
|
+
s = t.strength(x) if t.strength is not None else _head_strength(runner, t, encoded, x)
|
|
251
|
+
measured = mask & (mask.cumsum(-1) > int(unit.skip_first)) # protocol 0.8
|
|
252
|
+
s = s.float().masked_fill(~measured.to(s.device), -1.0)
|
|
253
|
+
peak_values, peaks = s.max(-1)
|
|
254
|
+
fired = peak_values.detach() >= threshold
|
|
255
|
+
if not fired.any():
|
|
256
|
+
return None
|
|
257
|
+
(grad,) = torch.autograd.grad(peak_values[fired].sum(), x)
|
|
258
|
+
return grad.float()[fired], peaks[fired], fired
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _add_upstream(acc: _Sums, runner, cap, grad, fired, up_n, up_h, w_out, w_o) -> None:
|
|
262
|
+
for i in up_n:
|
|
263
|
+
per_neuron = (grad @ w_out[i].to(grad.device)) * cap["acts"][i][fired]
|
|
264
|
+
acc.add(("up_n", i), per_neuron.sum((0, 1)))
|
|
265
|
+
for i in up_h:
|
|
266
|
+
z = cap["z"][i][fired].reshape(*grad.shape[:2], runner.n_heads, -1)
|
|
267
|
+
per_head = torch.einsum("bsd,ndh,bsnh->n", grad, w_o[i].to(grad.device), z)
|
|
268
|
+
acc.add(("up_h", i), per_head)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _add_downstream(acc: _Sums, runner, t, encoded, cap, fired, peaks, down_n, down_h, dtype):
|
|
272
|
+
delta = t.written(cap)
|
|
273
|
+
rows = torch.arange(len(peaks))
|
|
274
|
+
at_peak = delta[fired][rows, peaks]
|
|
275
|
+
for i in down_n:
|
|
276
|
+
resid = cap["mlp_in"][i][fired][rows, peaks]
|
|
277
|
+
both = torch.cat([resid, resid - at_peak]).to(dtype)
|
|
278
|
+
after = _mlp_activations(runner, i, both).float()
|
|
279
|
+
acc.add(("down_n", i), (after[: len(peaks)] - after[len(peaks) :]).sum(0))
|
|
280
|
+
if down_h: # empty without supported attention
|
|
281
|
+
sub = {key: v[fired] for key, v in encoded.items()}
|
|
282
|
+
kept = {**cap, "z": {i: v[fired] for i, v in cap["z"].items()}}
|
|
283
|
+
for i in down_h:
|
|
284
|
+
effects = _downstream_head_effects(runner, t, sub, kept, delta[fired], i, peaks)
|
|
285
|
+
acc.add(("down_h", i), effects.sum(0))
|
openinterp/langid.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""A document's language, for corpora whose dataset doesn't say it.
|
|
2
|
+
|
|
3
|
+
Used by ``open-interpretability corpus --detect-lang``. The detector is fastText's ``lid.176``
|
|
4
|
+
(compressed, 176 languages). We compared it with lingua, langdetect and langid.py on
|
|
5
|
+
labelled Wikipedia (``docs/analyses/language-detection.md``). It was the most accurate
|
|
6
|
+
at every length and the fastest, and it alone is unsure of source code.
|
|
7
|
+
|
|
8
|
+
A document gets a label only when the detector's confidence reaches ``MIN_CONFIDENCE``.
|
|
9
|
+
So code, markup and mixed text mostly stay unlabelled, and the scan leaves them out of
|
|
10
|
+
the language confound. Detection is per document, never per window, because a window of
|
|
11
|
+
a few dozen tokens is too short to detect reliably.
|
|
12
|
+
|
|
13
|
+
The model (CC BY-SA 3.0) is downloaded once, checked against ``MODEL_SHA256`` and cached
|
|
14
|
+
in ``$XDG_CACHE_HOME/openinterp`` (else ``~/.cache/openinterp``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import os
|
|
21
|
+
import urllib.request
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from functools import cache
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
MODEL_URL = "https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.ftz"
|
|
27
|
+
MODEL_SHA256 = "8f3472cfe8738a7b6099e8e999c3cbfae0dcd15696aac7d7738a8039db603e83"
|
|
28
|
+
# Below this the document is left unlabelled. On labelled Wikipedia it keeps 98.5% of
|
|
29
|
+
# documents at 99.9% accuracy, and labels 9% of source files (0.5 would label 45%).
|
|
30
|
+
MIN_CONFIDENCE = 0.8
|
|
31
|
+
DETECTOR = f"fasttext lid.176.ftz (sha256 {MODEL_SHA256[:12]}), confidence >= {MIN_CONFIDENCE}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def model_path() -> Path:
|
|
35
|
+
"""The cached model, downloaded and checked on first use."""
|
|
36
|
+
base = Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache")
|
|
37
|
+
path = base / "openinterp" / Path(MODEL_URL).name
|
|
38
|
+
if not path.exists():
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
with urllib.request.urlopen(MODEL_URL) as response: # noqa: S310 (fixed https URL)
|
|
41
|
+
data = response.read()
|
|
42
|
+
if hashlib.sha256(data).hexdigest() != MODEL_SHA256:
|
|
43
|
+
raise ValueError(f"{MODEL_URL} does not match its pinned SHA-256")
|
|
44
|
+
partial = path.with_suffix(".part")
|
|
45
|
+
partial.write_bytes(data)
|
|
46
|
+
partial.replace(path)
|
|
47
|
+
return path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@cache
|
|
51
|
+
def _model():
|
|
52
|
+
import fasttext
|
|
53
|
+
|
|
54
|
+
return fasttext.load_model(str(model_path()))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def predict(text: str) -> tuple[str, float]:
|
|
58
|
+
"""Return the most likely language of ``text`` and the detector's confidence in it.
|
|
59
|
+
|
|
60
|
+
The language is the model's code, for example ``"en"`` or ``"ceb"``.
|
|
61
|
+
"""
|
|
62
|
+
# fastText reads one line; ``f.predict`` returns [(probability, label)] without numpy.
|
|
63
|
+
((p, label),) = _model().f.predict(" ".join(text.split()), 1, 0.0, "strict")
|
|
64
|
+
return label.removeprefix("__label__"), float(p)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def detect(texts: Sequence[str]) -> list[str | None]:
|
|
68
|
+
"""Each document's language, or ``None`` below :data:`MIN_CONFIDENCE`."""
|
|
69
|
+
out: list[str | None] = []
|
|
70
|
+
for text in texts:
|
|
71
|
+
label, p = predict(text)
|
|
72
|
+
out.append(label if p >= MIN_CONFIDENCE else None)
|
|
73
|
+
return out
|
openinterp/names.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Find people's names in a document, and tag its words' parts of speech.
|
|
2
|
+
|
|
3
|
+
The names are the scan's ``names`` label set, and the tags its ``part of speech`` set
|
|
4
|
+
(issue #3223). One pass of the same spaCy model gives both.
|
|
5
|
+
|
|
6
|
+
The label is a model's guess, for experiments only. Nothing removes text because of it.
|
|
7
|
+
On a 500-document sample it matched names in 47% of documents. Most are public and
|
|
8
|
+
fictional people, so a match does not say that the text is personal data.
|
|
9
|
+
|
|
10
|
+
The detector is spaCy's ``en_core_web_trf``, the best of spaCy's four English models. On
|
|
11
|
+
CoNLL-2003's person names they had F1 0.67, 0.71, 0.78 and 0.89. We also compared
|
|
12
|
+
them on a sample of web text, code and other languages. There the smaller models also
|
|
13
|
+
called code and non-English phrases names. They matched 77%, 71% and 62% of documents.
|
|
14
|
+
This model is the slowest, at about 180 ms a document on CPU. So each document's result
|
|
15
|
+
stays in a cache on disk. It is an English model, so it reads other languages less well.
|
|
16
|
+
|
|
17
|
+
The model (MIT) is about 460 MB. It is downloaded once, checked against ``MODEL_SHA256``
|
|
18
|
+
and cached in ``$XDG_CACHE_HOME/openinterp`` (else ``~/.cache/openinterp``).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import shutil
|
|
27
|
+
import urllib.request
|
|
28
|
+
import zipfile
|
|
29
|
+
from functools import cache
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
MODEL = "en_core_web_trf-3.8.0"
|
|
33
|
+
MODEL_URL = (
|
|
34
|
+
f"https://github.com/explosion/spacy-models/releases/download/{MODEL}/{MODEL}-py3-none-any.whl"
|
|
35
|
+
)
|
|
36
|
+
MODEL_SHA256 = "272a31e9d8530d1e075351d30a462d7e80e31da23574f1b274e200f3fff35bf5"
|
|
37
|
+
DETECTOR = f"spaCy {MODEL} (sha256 {MODEL_SHA256[:12]})"
|
|
38
|
+
PERSON = "person" # the label of a match
|
|
39
|
+
# The parser and lemmatizer are slower, and neither label set needs them. The tagger and
|
|
40
|
+
# the attribute ruler give each word its universal part-of-speech tag (UPOS).
|
|
41
|
+
_PIPES = ("transformer", "tagger", "attribute_ruler", "ner")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _cache_dir() -> Path:
|
|
45
|
+
return Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "openinterp"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _download(wheel: Path) -> None:
|
|
49
|
+
"""Write the model's wheel to ``wheel``, or raise if it does not match its pinned SHA-256."""
|
|
50
|
+
digest = hashlib.sha256()
|
|
51
|
+
partial = wheel.with_suffix(".part")
|
|
52
|
+
with urllib.request.urlopen(MODEL_URL) as response, partial.open("wb") as out:
|
|
53
|
+
while chunk := response.read(1 << 20):
|
|
54
|
+
digest.update(chunk)
|
|
55
|
+
out.write(chunk)
|
|
56
|
+
if digest.hexdigest() != MODEL_SHA256:
|
|
57
|
+
partial.unlink()
|
|
58
|
+
raise ValueError(f"{MODEL_URL} does not match its pinned SHA-256")
|
|
59
|
+
partial.replace(wheel)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def model_path() -> Path:
|
|
63
|
+
"""The unpacked model, downloaded and checked on first use."""
|
|
64
|
+
root = _cache_dir() / MODEL
|
|
65
|
+
path = root / "en_core_web_trf" / MODEL
|
|
66
|
+
if not path.exists():
|
|
67
|
+
root.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
wheel = root.with_suffix(".whl")
|
|
69
|
+
_download(wheel)
|
|
70
|
+
unpacking = root.with_suffix(".unpacking")
|
|
71
|
+
shutil.rmtree(unpacking, ignore_errors=True)
|
|
72
|
+
with zipfile.ZipFile(wheel) as archive:
|
|
73
|
+
archive.extractall(unpacking)
|
|
74
|
+
unpacking.replace(root)
|
|
75
|
+
wheel.unlink()
|
|
76
|
+
return path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@cache
|
|
80
|
+
def _model():
|
|
81
|
+
import spacy
|
|
82
|
+
|
|
83
|
+
nlp = spacy.load(model_path())
|
|
84
|
+
return spacy.load(model_path(), exclude=[p for p in nlp.pipe_names if p not in _PIPES])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _cached(kind: str, text: str) -> Path:
|
|
88
|
+
key = hashlib.sha256(text.encode()).hexdigest()
|
|
89
|
+
return _cache_dir() / kind / MODEL_SHA256[:12] / f"{key}.json"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _write(path: Path, value) -> None:
|
|
93
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
partial = path.with_suffix(f".{os.getpid()}.part") # scans can run side by side
|
|
95
|
+
partial.write_text(json.dumps(value))
|
|
96
|
+
partial.replace(path)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _annotate(text: str) -> None:
|
|
100
|
+
"""Run the model on ``text`` once, and cache both its names and its words' tags."""
|
|
101
|
+
doc = _model()(text)
|
|
102
|
+
names = sorted((e.start_char, e.end_char) for e in doc.ents if e.label_ == "PERSON")
|
|
103
|
+
tags = [(t.idx, t.idx + len(t.text), t.pos_) for t in doc if t.pos_ and not t.is_space]
|
|
104
|
+
_write(_cached("names", text), names)
|
|
105
|
+
_write(_cached("pos", text), tags)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def person_matches(text: str) -> tuple[tuple[int, int, str], ...]:
|
|
109
|
+
"""Return ``(start, end, PERSON)`` of each name in ``text``, sorted.
|
|
110
|
+
|
|
111
|
+
The result stays on disk by the text's SHA-256, so a later scan of a corpus does not
|
|
112
|
+
run the model again.
|
|
113
|
+
"""
|
|
114
|
+
path = _cached("names", text)
|
|
115
|
+
if not path.exists():
|
|
116
|
+
_annotate(text)
|
|
117
|
+
return tuple((start, end, PERSON) for start, end in json.loads(path.read_text()))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def pos_matches(text: str) -> tuple[tuple[int, int, str], ...]:
|
|
121
|
+
"""Return ``(start, end, UPOS)`` of each word of ``text``, in order (issue #3223).
|
|
122
|
+
|
|
123
|
+
Whitespace has no tag. A document whose names are already cached gets its tags from
|
|
124
|
+
one more pass of the model, and both stay on disk after that.
|
|
125
|
+
"""
|
|
126
|
+
path = _cached("pos", text)
|
|
127
|
+
if not path.exists():
|
|
128
|
+
_annotate(text)
|
|
129
|
+
return tuple((start, end, tag) for start, end, tag in json.loads(path.read_text()))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Neuronpedia's explanations as a blind proposer baseline.
|
|
2
|
+
|
|
3
|
+
The command is `open-interpretability neuronpedia-baseline`.
|
|
4
|
+
|
|
5
|
+
Does Neuronpedia's explanation of a dictionary latent explain it as well as the agent's
|
|
6
|
+
hypothesis? The rule was fixed before scoring. Each experiment on a latent that
|
|
7
|
+
Neuronpedia hosts takes the latent's first machine-written explanation, word for word
|
|
8
|
+
(:func:`openinterp.crossref.machine_explanation`). Human-written ones are
|
|
9
|
+
left out, so the baseline is one automatic explainer.
|
|
10
|
+
|
|
11
|
+
The explanation is then scored on the record's own held-out windows, so the two scores
|
|
12
|
+
are paired. The Patchscope baseline is scored the same way (:mod:`openinterp.baseline`).
|
|
13
|
+
Nothing is sent to Neuronpedia. The explanations are read once through its public API
|
|
14
|
+
and kept in the cache of :mod:`openinterp.crossref`. Each row credits Neuronpedia and the model that
|
|
15
|
+
wrote the explanation, and links to the latent's page. The author's account is not kept.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections import Counter
|
|
21
|
+
from collections.abc import Sequence
|
|
22
|
+
|
|
23
|
+
from openinterp.baseline import METRICS, BaselineRow, paired_report
|
|
24
|
+
|
|
25
|
+
CREDIT = "Neuronpedia"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def reading(explainer: str | None) -> str:
|
|
29
|
+
"""The ``reading`` of a row: who wrote the baseline, or that there was none."""
|
|
30
|
+
return f"{CREDIT}, {explainer}" if explainer else f"no {CREDIT} explanation"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def report(rows: Sequence[BaselineRow], links: dict[str, str | None]) -> str:
|
|
34
|
+
"""Return the comparison as a Markdown page.
|
|
35
|
+
|
|
36
|
+
``links`` maps each row's slug to its latent's Neuronpedia page. The page gives the
|
|
37
|
+
rule, the paired table, and every explanation with its credit and link.
|
|
38
|
+
"""
|
|
39
|
+
have = [r for r in rows if r.baseline_explanation is not None]
|
|
40
|
+
judged = sum(r.baseline_judge_model is not None for r in rows)
|
|
41
|
+
units = {r.slug for r in rows}
|
|
42
|
+
covered = {r.slug for r in have}
|
|
43
|
+
explainers = Counter(r.reading.removeprefix(f"{CREDIT}, ") for r in have)
|
|
44
|
+
credits = ", ".join(f"{k} on {n} experiments" for k, n in explainers.items()) or "none"
|
|
45
|
+
intro = (
|
|
46
|
+
f"Explanations from [Neuronpedia](https://www.neuronpedia.org), credited to the "
|
|
47
|
+
f"model that wrote them ({credits}). "
|
|
48
|
+
f"The rule, fixed before scoring: each latent's first machine-written explanation, "
|
|
49
|
+
f"word for word. Human-written ones are left out. Neuronpedia has one for "
|
|
50
|
+
f"{len(covered)} of the {len(units)} audited latents it hosts. Each is scored on the "
|
|
51
|
+
f"same held-out windows as the agent's explanation. Detection and fuzzing pair only "
|
|
52
|
+
f"experiments whose agent explanation was judged by the same model as the "
|
|
53
|
+
f"baseline's ({judged} of {len(rows)}). Embedding uses the record's own encoder. "
|
|
54
|
+
f"The sign test counts experiments, and a unit with several experiments counts "
|
|
55
|
+
f"more than once."
|
|
56
|
+
)
|
|
57
|
+
after = [
|
|
58
|
+
"Per experiment, agent → Neuronpedia on each metric (— where unpaired). The "
|
|
59
|
+
"agent's explanation is on the unit's page.",
|
|
60
|
+
"",
|
|
61
|
+
"| Unit | Status | Neuronpedia's explanation | Detection | Fuzzing | Embedding |",
|
|
62
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
63
|
+
]
|
|
64
|
+
for r in sorted(rows, key=lambda r: (r.slug, r.experiment_id)):
|
|
65
|
+
link = links.get(r.slug)
|
|
66
|
+
unit = f"[{r.slug}]({link})" if link else r.slug
|
|
67
|
+
theirs = _cell(r.baseline_explanation) if r.baseline_explanation else "—"
|
|
68
|
+
pairs = " | ".join(_pair(r, name) for name in METRICS)
|
|
69
|
+
after.append(f"| {unit} | {r.status} | {theirs} | {pairs} |")
|
|
70
|
+
return paired_report(
|
|
71
|
+
rows,
|
|
72
|
+
title="Neuronpedia explanations as a blind proposer baseline",
|
|
73
|
+
generated_by="`open-interpretability neuronpedia-baseline`",
|
|
74
|
+
method="`openinterp/neuronpedia_baseline.py` and docs/PROTOCOL.md §4",
|
|
75
|
+
intro=intro,
|
|
76
|
+
after=after,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _pair(row: BaselineRow, name: str) -> str:
|
|
81
|
+
"""``agent → baseline`` on metric ``name``, or ``—`` if either is missing."""
|
|
82
|
+
a, b = row.agent[name], row.baseline[name]
|
|
83
|
+
return "—" if a is None or b is None else f"{a:.2f} → {b:.2f}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _cell(text: str) -> str:
|
|
87
|
+
"""``text`` made safe for one Markdown table cell."""
|
|
88
|
+
return " ".join(text.split()).replace("|", "\\|").replace("<", "<")
|