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,463 @@
|
|
|
1
|
+
"""The sister search over many model pairs, with each model read once per stage.
|
|
2
|
+
|
|
3
|
+
:func:`~openinterp.harness.sisters.find_sisters` gives the method. A search per pair reads
|
|
4
|
+
both models over the whole corpus twice, so its cost grows with the number of pairs. This
|
|
5
|
+
module splits the same computation into four stages:
|
|
6
|
+
|
|
7
|
+
1. **Source series.** Each source model runs to its deepest new pole. Each pole's value
|
|
8
|
+
at every token goes to the CPU.
|
|
9
|
+
2. **Forward.** Each other model runs over every layer. Each layer is correlated with
|
|
10
|
+
every incoming series, at the boundaries that the two tokenizations share.
|
|
11
|
+
3. **Match series and rotations.** Each other model runs to the deepest layer needed. It
|
|
12
|
+
keeps each match's series, and correlates each source pole with a rotated basis of its
|
|
13
|
+
top match's layer. Rotations go a few layers at a time, under a memory budget.
|
|
14
|
+
4. **Backward.** Each source model runs over every layer, correlated with the incoming
|
|
15
|
+
match series.
|
|
16
|
+
|
|
17
|
+
Each stage loads one model at a time and frees it before the next. Hooks keep only the
|
|
18
|
+
rows that a stage reads, not the whole ``[batch, seq, width]`` activation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import gc
|
|
24
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
25
|
+
from contextlib import suppress
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
|
|
28
|
+
import torch
|
|
29
|
+
|
|
30
|
+
from openinterp.harness.hooks import NeuronHookRunner, _StopForward
|
|
31
|
+
from openinterp.harness.scan import BATCH_TOKENS, plan_batches
|
|
32
|
+
from openinterp.harness.sisters import (
|
|
33
|
+
Correlations,
|
|
34
|
+
Sisters,
|
|
35
|
+
Tokens,
|
|
36
|
+
Unit,
|
|
37
|
+
_top,
|
|
38
|
+
pole_columns,
|
|
39
|
+
rotation,
|
|
40
|
+
shared_boundaries,
|
|
41
|
+
tokenize,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# A model as a stage loads it: (name, revision).
|
|
45
|
+
ModelKey = tuple[str, str | None]
|
|
46
|
+
# The largest total size of the rotations that one pass holds on the device.
|
|
47
|
+
ROTATION_BYTES = 2 * 1024**3
|
|
48
|
+
# The largest total size of the correlation sums that one pass keeps, on the CPU.
|
|
49
|
+
CORRELATION_BYTES = 3 * 1024**3
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Job:
|
|
54
|
+
"""One ordered pair: the ``units`` of ``source`` to match in ``other``.
|
|
55
|
+
|
|
56
|
+
The search fills in the fields after ``other_name``. ``other_name`` seeds the
|
|
57
|
+
rotations, as ``find_sisters`` does.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
source: ModelKey
|
|
61
|
+
other: ModelKey
|
|
62
|
+
units: list[Unit]
|
|
63
|
+
other_name: str
|
|
64
|
+
found: dict[Unit, Sisters] = field(default_factory=dict)
|
|
65
|
+
tokens: int = 0 # positions paired
|
|
66
|
+
readable: int = 0 # the source's tokens, less each text's first
|
|
67
|
+
source_revision: str | None = None
|
|
68
|
+
other_revision: str | None = None
|
|
69
|
+
source_layers: int = 0
|
|
70
|
+
other_layers: int = 0
|
|
71
|
+
# Working state between stages.
|
|
72
|
+
rows: list[tuple[list[int], list[int]]] = field(default_factory=list) # per document
|
|
73
|
+
matches: list[list] = field(default_factory=list)
|
|
74
|
+
match_units: list[Unit] = field(default_factory=list)
|
|
75
|
+
match_series: list[torch.Tensor | None] = field(default_factory=list) # per document
|
|
76
|
+
back: list = field(default_factory=list) # per match unit: its best source partner
|
|
77
|
+
by_layer: dict[int, list[int]] = field(default_factory=dict)
|
|
78
|
+
rotated: dict[int, Correlations] = field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class _Model:
|
|
83
|
+
"""What the search keeps of one model between stages."""
|
|
84
|
+
|
|
85
|
+
tokens: list[Tokens] = field(default_factory=list)
|
|
86
|
+
units: list[Unit] = field(default_factory=list) # its new poles, as a source
|
|
87
|
+
series: list[torch.Tensor | None] = field(default_factory=list) # per document
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def search(
|
|
91
|
+
jobs: Sequence[Job],
|
|
92
|
+
load: Callable[[str, str | None], NeuronHookRunner],
|
|
93
|
+
texts: Sequence[str],
|
|
94
|
+
*,
|
|
95
|
+
k: int = 5,
|
|
96
|
+
batch_tokens: int = BATCH_TOKENS,
|
|
97
|
+
rotation_bytes: int = ROTATION_BYTES,
|
|
98
|
+
correlation_bytes: int = CORRELATION_BYTES,
|
|
99
|
+
progress: Callable[[str, int], None] | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Fill in each job's sisters, in four stages over the corpus (module docstring)."""
|
|
102
|
+
run = _Stages(
|
|
103
|
+
list(jobs), load, texts, k, batch_tokens, rotation_bytes, correlation_bytes, progress
|
|
104
|
+
)
|
|
105
|
+
for key in _keys(j.source for j in jobs):
|
|
106
|
+
run.source_series(key)
|
|
107
|
+
for key in _keys(j.other for j in jobs):
|
|
108
|
+
run.forward(key)
|
|
109
|
+
for key in _keys(j.other for j in jobs):
|
|
110
|
+
run.match_series(key)
|
|
111
|
+
for key in _keys(j.source for j in jobs):
|
|
112
|
+
run.backward(key)
|
|
113
|
+
for job in jobs:
|
|
114
|
+
job.found = run.result(job)
|
|
115
|
+
job.rows, job.match_series, job.rotated, job.back = [], [], {}, []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _keys(keys) -> list[ModelKey]:
|
|
119
|
+
return list(dict.fromkeys(keys))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class _Stages:
|
|
124
|
+
jobs: list[Job]
|
|
125
|
+
load: Callable[[str, str | None], NeuronHookRunner]
|
|
126
|
+
texts: Sequence[str]
|
|
127
|
+
k: int
|
|
128
|
+
batch_tokens: int
|
|
129
|
+
rotation_bytes: int
|
|
130
|
+
correlation_bytes: int
|
|
131
|
+
progress: Callable[[str, int], None] | None
|
|
132
|
+
models: dict[ModelKey, _Model] = field(default_factory=dict)
|
|
133
|
+
|
|
134
|
+
def tick(self, stage: str, n: int) -> None:
|
|
135
|
+
if self.progress:
|
|
136
|
+
self.progress(stage, n)
|
|
137
|
+
|
|
138
|
+
def source_series(self, key: ModelKey) -> None:
|
|
139
|
+
"""Stage 1: each new pole of ``key`` at every token of every text."""
|
|
140
|
+
jobs = [j for j in self.jobs if j.source == key]
|
|
141
|
+
runner = self.load(*key)
|
|
142
|
+
model = self.models[key] = _Model([tokenize(runner, t) for t in self.texts])
|
|
143
|
+
model.units = list(dict.fromkeys(u for j in jobs for u in j.units))
|
|
144
|
+
everything = [list(range(len(t.ids))) for t in model.tokens]
|
|
145
|
+
model.series = [None] * len(self.texts)
|
|
146
|
+
columns = {}
|
|
147
|
+
for layer, idx, _ in model.units:
|
|
148
|
+
columns.setdefault(layer, []).append(idx)
|
|
149
|
+
for docs, rows in _passes(runner, model.tokens, everything, columns, self.batch_tokens):
|
|
150
|
+
series = _strengths(rows, columns, model.units)
|
|
151
|
+
for d, part in _split(series, docs, [len(everything[d]) for d in docs]):
|
|
152
|
+
model.series[d] = part
|
|
153
|
+
self.tick("source", sum(len(everything[d]) for d in docs))
|
|
154
|
+
readable = sum(max(len(t.ids) - 1, 0) for t in model.tokens)
|
|
155
|
+
for job in jobs:
|
|
156
|
+
job.readable, job.source_layers = readable, runner.n_layers
|
|
157
|
+
job.source_revision = _revision(runner)
|
|
158
|
+
del runner
|
|
159
|
+
_free()
|
|
160
|
+
|
|
161
|
+
def forward(self, key: ModelKey) -> None:
|
|
162
|
+
"""Stage 2: each job's ``k`` best poles over every layer of ``key``."""
|
|
163
|
+
jobs = [j for j in self.jobs if j.other == key]
|
|
164
|
+
runner = self.load(*key)
|
|
165
|
+
tokens = self.models.setdefault(key, _Model()).tokens or [
|
|
166
|
+
tokenize(runner, t) for t in self.texts
|
|
167
|
+
]
|
|
168
|
+
self.models[key].tokens = tokens
|
|
169
|
+
for job in jobs:
|
|
170
|
+
src = self.models[job.source].tokens
|
|
171
|
+
job.rows = [_boundaries(a, b) for a, b in zip(src, tokens, strict=True)]
|
|
172
|
+
job.other_layers, job.other_revision = runner.n_layers, _revision(runner)
|
|
173
|
+
every = list(range(runner.n_layers))
|
|
174
|
+
corr = self._correlate(runner, tokens, jobs, "forward")
|
|
175
|
+
widths = {layer: runner.mlp_width(layer) for layer in every}
|
|
176
|
+
for job in jobs:
|
|
177
|
+
job.matches = _top(corr[id(job)], every, widths, self.k)
|
|
178
|
+
job.tokens = corr[id(job)].n
|
|
179
|
+
del runner
|
|
180
|
+
_free()
|
|
181
|
+
|
|
182
|
+
def match_series(self, key: ModelKey) -> None:
|
|
183
|
+
"""Stage 3: each match's series, and each pole's rotated reference."""
|
|
184
|
+
jobs = [j for j in self.jobs if j.other == key]
|
|
185
|
+
for job in jobs:
|
|
186
|
+
job.match_units = list(
|
|
187
|
+
dict.fromkeys((m.layer, m.neuron_idx, m.pole) for ms in job.matches for m in ms)
|
|
188
|
+
)
|
|
189
|
+
job.match_series = [None] * len(self.texts)
|
|
190
|
+
job.by_layer = {}
|
|
191
|
+
for i, ms in enumerate(job.matches):
|
|
192
|
+
if ms:
|
|
193
|
+
job.by_layer.setdefault(ms[0].layer, []).append(i)
|
|
194
|
+
job.rotated = {layer: Correlations(len(i)) for layer, i in job.by_layer.items()}
|
|
195
|
+
runner = self.load(*key)
|
|
196
|
+
tokens = self.models[key].tokens
|
|
197
|
+
turned = sorted({layer for j in jobs for layer in j.by_layer})
|
|
198
|
+
for n, group in enumerate(_groups(turned, runner, self.rotation_bytes) or [[]]):
|
|
199
|
+
self._match_pass(runner, jobs, tokens, group, first=n == 0)
|
|
200
|
+
del runner
|
|
201
|
+
_free()
|
|
202
|
+
|
|
203
|
+
def _match_pass(self, runner, jobs, tokens, group, first: bool) -> None:
|
|
204
|
+
"""One pass of stage 3: the match series (on the first pass) and ``group``'s turns."""
|
|
205
|
+
name = jobs[0].other_name
|
|
206
|
+
turns = {
|
|
207
|
+
layer: rotation(name, layer, runner.mlp_width(layer)).to(runner.device)
|
|
208
|
+
for layer in group
|
|
209
|
+
}
|
|
210
|
+
layers = set(group) | ({u[0] for j in jobs for u in j.match_units} if first else set())
|
|
211
|
+
if not layers:
|
|
212
|
+
return
|
|
213
|
+
wanted = _union(tokens, [[r[1] for r in j.rows] for j in jobs])
|
|
214
|
+
columns = dict.fromkeys(layers)
|
|
215
|
+
for docs, rows in _passes(runner, tokens, wanted, columns, self.batch_tokens):
|
|
216
|
+
for job in jobs:
|
|
217
|
+
sel = _select(docs, wanted, [job.rows[d][1] for d in docs])
|
|
218
|
+
reads = {layer for layer in group if layer in job.by_layer}
|
|
219
|
+
reads |= {u[0] for u in job.match_units} if first else set()
|
|
220
|
+
picked = {layer: _take(rows[layer], sel) for layer in reads}
|
|
221
|
+
if first:
|
|
222
|
+
series = _strengths(picked, None, job.match_units)
|
|
223
|
+
counts = [len(job.rows[d][1]) for d in docs]
|
|
224
|
+
for d, part in _split(series, docs, counts):
|
|
225
|
+
job.match_series[d] = part
|
|
226
|
+
own = self._sources(job, docs, job.units).to(runner.device)
|
|
227
|
+
for layer in group:
|
|
228
|
+
if layer in job.by_layer:
|
|
229
|
+
xs = own[:, job.by_layer[layer]]
|
|
230
|
+
job.rotated[layer].add_sources(xs)
|
|
231
|
+
job.rotated[layer].add_targets(
|
|
232
|
+
layer, xs, pole_columns(picked[layer] @ turns[layer])
|
|
233
|
+
)
|
|
234
|
+
self.tick("matches", sum(len(wanted[d]) for d in docs))
|
|
235
|
+
|
|
236
|
+
def backward(self, key: ModelKey) -> None:
|
|
237
|
+
"""Stage 4: each match's best partner over every layer of the source ``key``."""
|
|
238
|
+
jobs = [j for j in self.jobs if j.source == key]
|
|
239
|
+
runner = self.load(*key)
|
|
240
|
+
tokens = self.models[key].tokens
|
|
241
|
+
every = list(range(runner.n_layers))
|
|
242
|
+
back = self._correlate(runner, tokens, jobs, "backward")
|
|
243
|
+
widths = {layer: runner.mlp_width(layer) for layer in every}
|
|
244
|
+
for job in jobs:
|
|
245
|
+
best = _top(back[id(job)], every, widths, 1)
|
|
246
|
+
job.back = [m[0] for m in best]
|
|
247
|
+
del runner
|
|
248
|
+
_free()
|
|
249
|
+
|
|
250
|
+
def _correlate(self, runner, tokens, jobs: list[Job], stage: str) -> dict[int, Correlations]:
|
|
251
|
+
"""Correlate every layer of ``runner`` with each job's incoming series.
|
|
252
|
+
|
|
253
|
+
In the forward stage the series are the source poles, at the other model's side of
|
|
254
|
+
each boundary. In the backward stage they are the matches, at the source's side.
|
|
255
|
+
Jobs go in groups whose running totals fit :data:`CORRELATION_BYTES`, one pass each.
|
|
256
|
+
"""
|
|
257
|
+
every = list(range(runner.n_layers))
|
|
258
|
+
side = 1 if stage == "forward" else 0
|
|
259
|
+
columns = 2 * sum(runner.mlp_width(layer) for layer in every)
|
|
260
|
+
corr: dict[int, Correlations] = {}
|
|
261
|
+
for group in _job_groups(jobs, columns, self.correlation_bytes, stage):
|
|
262
|
+
group_corr = {id(j): Correlations(_width(j, stage)) for j in group}
|
|
263
|
+
wanted = _union(tokens, [[r[side] for r in j.rows] for j in group])
|
|
264
|
+
columns_ = dict.fromkeys(every)
|
|
265
|
+
for docs, rows in _passes(runner, tokens, wanted, columns_, self.batch_tokens):
|
|
266
|
+
for job in group:
|
|
267
|
+
x = self._incoming(job, docs, stage).to(runner.device)
|
|
268
|
+
sel = _select(docs, wanted, [job.rows[d][side] for d in docs])
|
|
269
|
+
group_corr[id(job)].add_sources(x)
|
|
270
|
+
for layer in every:
|
|
271
|
+
group_corr[id(job)].add_targets(
|
|
272
|
+
layer, x, pole_columns(_take(rows[layer], sel))
|
|
273
|
+
)
|
|
274
|
+
self.tick(stage, group_corr[id(group[0])].n)
|
|
275
|
+
corr.update(group_corr)
|
|
276
|
+
return corr
|
|
277
|
+
|
|
278
|
+
def _incoming(self, job: Job, docs: list[int], stage: str) -> torch.Tensor:
|
|
279
|
+
if stage == "forward":
|
|
280
|
+
return self._sources(job, docs, job.units)
|
|
281
|
+
return _cat([job.match_series[d] for d in docs], len(job.match_units))
|
|
282
|
+
|
|
283
|
+
def _sources(self, job: Job, docs: list[int], units: list[Unit]) -> torch.Tensor:
|
|
284
|
+
"""``[n, len(units)]``: the source's series at ``job``'s boundaries in ``docs``."""
|
|
285
|
+
model = self.models[job.source]
|
|
286
|
+
cols = [model.units.index(u) for u in units]
|
|
287
|
+
parts = [
|
|
288
|
+
model.series[d][job.rows[d][0]][:, cols]
|
|
289
|
+
for d in docs
|
|
290
|
+
if job.rows[d][0] and model.series[d] is not None
|
|
291
|
+
]
|
|
292
|
+
return torch.cat(parts) if parts else torch.zeros(0, len(cols))
|
|
293
|
+
|
|
294
|
+
def result(self, job: Job) -> dict[Unit, Sisters]:
|
|
295
|
+
"""Each of ``job.units`` with its matches, back partners and rotated reference."""
|
|
296
|
+
back_of = dict(zip(job.match_units, job.back, strict=True))
|
|
297
|
+
reference = {}
|
|
298
|
+
for layer, idx in job.by_layer.items():
|
|
299
|
+
r = job.rotated[layer].pearson(layer).amax(dim=1)
|
|
300
|
+
reference.update({i: float(v) for i, v in zip(idx, r.tolist(), strict=True)})
|
|
301
|
+
return {
|
|
302
|
+
unit: Sisters(
|
|
303
|
+
job.matches[i],
|
|
304
|
+
[back_of[(m.layer, m.neuron_idx, m.pole)] for m in job.matches[i]],
|
|
305
|
+
reference.get(i, 0.0),
|
|
306
|
+
)
|
|
307
|
+
for i, unit in enumerate(job.units)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _boundaries(a: Tokens, b: Tokens) -> tuple[list[int], list[int]]:
|
|
312
|
+
"""The shared boundaries of one text, or none if either model reads no token of it."""
|
|
313
|
+
return shared_boundaries(a.ends, b.ends) if a.ids and b.ids else ([], [])
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _union(tokens: list[Tokens], per_job: list[list[list[int]]]) -> list[list[int]]:
|
|
317
|
+
"""Per document: the sorted positions that any job reads."""
|
|
318
|
+
return [sorted({p for job in per_job for p in job[d]}) for d in range(len(tokens))]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _select(docs: list[int], wanted: list[list[int]], chosen: list[list[int]]) -> torch.Tensor:
|
|
322
|
+
"""Indices into a pass's rows (``wanted`` of ``docs``, in order) of the ``chosen`` ones."""
|
|
323
|
+
out, offset = [], 0
|
|
324
|
+
for d, picks in zip(docs, chosen, strict=True):
|
|
325
|
+
where = {p: i for i, p in enumerate(wanted[d])}
|
|
326
|
+
out += [offset + where[p] for p in picks]
|
|
327
|
+
offset += len(wanted[d])
|
|
328
|
+
return torch.tensor(out, dtype=torch.long)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _take(rows: torch.Tensor, sel: torch.Tensor) -> torch.Tensor:
|
|
332
|
+
"""``rows[sel]``, without a copy when ``sel`` is every row in order."""
|
|
333
|
+
if sel.shape[0] == rows.shape[0] and bool((sel == torch.arange(sel.shape[0])).all()):
|
|
334
|
+
return rows
|
|
335
|
+
return rows[sel.to(rows.device)]
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _split(series: torch.Tensor, docs: list[int], counts: list[int]):
|
|
339
|
+
"""Cut a pass's ``[n, P]`` rows into one CPU block per document, in ``docs`` order."""
|
|
340
|
+
offset = 0
|
|
341
|
+
for d, n in zip(docs, counts, strict=True):
|
|
342
|
+
yield d, series[offset : offset + n].cpu()
|
|
343
|
+
offset += n
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _cat(parts: list[torch.Tensor | None], width: int) -> torch.Tensor:
|
|
347
|
+
parts = [p for p in parts if p is not None and p.shape[0]]
|
|
348
|
+
return torch.cat(parts) if parts else torch.zeros(0, width)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _strengths(rows: dict, columns: dict | None, units: list[Unit]) -> torch.Tensor:
|
|
352
|
+
"""``[n, len(units)]`` pole strengths from a pass's rows.
|
|
353
|
+
|
|
354
|
+
With ``columns``, each layer's rows hold only the listed neurons, in that order.
|
|
355
|
+
"""
|
|
356
|
+
cols = []
|
|
357
|
+
for layer, idx, pole in units:
|
|
358
|
+
at = columns[layer].index(idx) if columns else idx
|
|
359
|
+
act = rows[layer][:, at]
|
|
360
|
+
cols.append(act.clamp(min=0) if pole == "positive" else (-act).clamp(min=0))
|
|
361
|
+
return torch.stack(cols, dim=1)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _width(job: Job, stage: str) -> int:
|
|
365
|
+
return len(job.units) if stage == "forward" else len(job.match_units)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _job_groups(jobs: list[Job], columns: int, budget: int, stage: str) -> list[list[Job]]:
|
|
369
|
+
"""``jobs`` in order, cut into groups whose running totals fit in ``budget`` bytes.
|
|
370
|
+
|
|
371
|
+
Each source series costs ``columns`` float64 sums (:class:`Correlations`). A job over
|
|
372
|
+
the budget on its own gets a group of its own.
|
|
373
|
+
"""
|
|
374
|
+
groups, size = [], budget + 1
|
|
375
|
+
for job in jobs:
|
|
376
|
+
need = _width(job, stage) * columns * 8
|
|
377
|
+
if size + need > budget:
|
|
378
|
+
groups.append([])
|
|
379
|
+
size = 0
|
|
380
|
+
groups[-1].append(job)
|
|
381
|
+
size += need
|
|
382
|
+
return groups
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _groups(layers: list[int], runner, budget: int) -> list[list[int]]:
|
|
386
|
+
"""``layers`` in order, cut into groups whose rotations fit in ``budget`` bytes."""
|
|
387
|
+
groups, size = [], budget + 1
|
|
388
|
+
for layer in layers:
|
|
389
|
+
need = runner.mlp_width(layer) ** 2 * 4
|
|
390
|
+
if size + need > budget:
|
|
391
|
+
groups.append([])
|
|
392
|
+
size = 0
|
|
393
|
+
groups[-1].append(layer)
|
|
394
|
+
size += need
|
|
395
|
+
return groups
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _passes(
|
|
399
|
+
runner: NeuronHookRunner,
|
|
400
|
+
tokens: list[Tokens],
|
|
401
|
+
wanted: list[list[int]],
|
|
402
|
+
columns: dict[int, list[int] | None],
|
|
403
|
+
batch_tokens: int,
|
|
404
|
+
) -> Iterator[tuple[list[int], dict[int, torch.Tensor]]]:
|
|
405
|
+
"""Run every text with a wanted position, and yield (documents, ``{layer: rows}``).
|
|
406
|
+
|
|
407
|
+
The rows are each document's ``wanted`` positions, in order. Where ``columns`` lists
|
|
408
|
+
neurons for a layer, only those are kept. The pass stops after the deepest layer.
|
|
409
|
+
"""
|
|
410
|
+
docs = [d for d, t in enumerate(tokens) if t.ids and wanted[d]]
|
|
411
|
+
lengths = [len(tokens[d].ids) for d in docs]
|
|
412
|
+
start = 0
|
|
413
|
+
for size in plan_batches(lengths, batch_tokens):
|
|
414
|
+
group, start = docs[start : start + size], start + size
|
|
415
|
+
yield (
|
|
416
|
+
group,
|
|
417
|
+
_gather(runner, [tokens[d].ids for d in group], [wanted[d] for d in group], columns),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _gather(runner, seqs, positions, columns) -> dict[int, torch.Tensor]:
|
|
422
|
+
"""Run ``seqs`` (right-padded), keeping only ``positions`` inside each layer's hook."""
|
|
423
|
+
width = max(map(len, seqs))
|
|
424
|
+
pad = getattr(runner.tokenizer, "pad_token_id", None) or 0
|
|
425
|
+
ids = torch.tensor([s + [pad] * (width - len(s)) for s in seqs])
|
|
426
|
+
mask = torch.tensor([[1] * len(s) + [0] * (width - len(s)) for s in seqs])
|
|
427
|
+
rows = [(row, p) for row, pos in enumerate(positions) for p in pos]
|
|
428
|
+
index = torch.tensor(rows, dtype=torch.long).reshape(-1, 2).to(runner.device)
|
|
429
|
+
keep = {
|
|
430
|
+
layer: None if cols is None else torch.tensor(cols, device=runner.device)
|
|
431
|
+
for layer, cols in columns.items()
|
|
432
|
+
}
|
|
433
|
+
out: dict[int, torch.Tensor] = {}
|
|
434
|
+
last = max(columns)
|
|
435
|
+
|
|
436
|
+
def recorder(layer: int):
|
|
437
|
+
def record(act: torch.Tensor) -> None:
|
|
438
|
+
picked = act[index[:, 0], index[:, 1]]
|
|
439
|
+
out[layer] = (picked if keep[layer] is None else picked[:, keep[layer]]).float()
|
|
440
|
+
if layer == last:
|
|
441
|
+
raise _StopForward
|
|
442
|
+
|
|
443
|
+
return record
|
|
444
|
+
|
|
445
|
+
hooks = runner.hooks({layer: recorder(layer) for layer in columns})
|
|
446
|
+
with torch.no_grad(), suppress(_StopForward), hooks:
|
|
447
|
+
runner.forward(runner.encode({"input_ids": ids, "attention_mask": mask}))
|
|
448
|
+
return out
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _revision(runner) -> str | None:
|
|
452
|
+
from openinterp.harness.environment import run_environment
|
|
453
|
+
|
|
454
|
+
return run_environment(runner).model_revision
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _free() -> None:
|
|
458
|
+
"""Collect the model that a stage dropped, and empty the device's cache."""
|
|
459
|
+
gc.collect()
|
|
460
|
+
if torch.backends.mps.is_available():
|
|
461
|
+
torch.mps.empty_cache()
|
|
462
|
+
if torch.cuda.is_available():
|
|
463
|
+
torch.cuda.empty_cache()
|