open-interpretability 0.0.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. open_interpretability-0.0.20.dist-info/METADATA +157 -0
  2. open_interpretability-0.0.20.dist-info/RECORD +256 -0
  3. open_interpretability-0.0.20.dist-info/WHEEL +4 -0
  4. open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
  5. open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
  6. openinterp/__init__.py +6 -0
  7. openinterp/agents/__init__.py +25 -0
  8. openinterp/agents/base.py +229 -0
  9. openinterp/agents/judge.py +149 -0
  10. openinterp/agents/proposer.py +471 -0
  11. openinterp/agents/proposer_profiles.py +79 -0
  12. openinterp/agents/review_measurements.py +173 -0
  13. openinterp/agents/reviewer.py +498 -0
  14. openinterp/agreement.py +63 -0
  15. openinterp/audit_options.py +79 -0
  16. openinterp/baseline.py +254 -0
  17. openinterp/benchmark.py +450 -0
  18. openinterp/blends.py +239 -0
  19. openinterp/builder/__init__.py +6 -0
  20. openinterp/builder/agents.py +260 -0
  21. openinterp/builder/aligned.py +41 -0
  22. openinterp/builder/charts.py +411 -0
  23. openinterp/builder/circuit_pages.py +37 -0
  24. openinterp/builder/circuits.py +264 -0
  25. openinterp/builder/compiler.py +495 -0
  26. openinterp/builder/corpora.py +98 -0
  27. openinterp/builder/coverage.py +46 -0
  28. openinterp/builder/dashboard.py +255 -0
  29. openinterp/builder/disk_memo.py +65 -0
  30. openinterp/builder/experiment.py +105 -0
  31. openinterp/builder/heat.py +121 -0
  32. openinterp/builder/hrefs.py +71 -0
  33. openinterp/builder/learned_baseline.py +204 -0
  34. openinterp/builder/model_facts.py +138 -0
  35. openinterp/builder/neuronpedia_export.py +107 -0
  36. openinterp/builder/parallel.py +100 -0
  37. openinterp/builder/parquet_export.py +458 -0
  38. openinterp/builder/pieces.py +127 -0
  39. openinterp/builder/pii.py +211 -0
  40. openinterp/builder/registry_stats.py +144 -0
  41. openinterp/builder/research.py +109 -0
  42. openinterp/builder/sections.py +28 -0
  43. openinterp/builder/significance.py +103 -0
  44. openinterp/builder/sister_graph.py +180 -0
  45. openinterp/builder/standing.py +185 -0
  46. openinterp/builder/statuses.py +34 -0
  47. openinterp/builder/stopped.py +91 -0
  48. openinterp/builder/stops.py +30 -0
  49. openinterp/builder/superpositions.py +102 -0
  50. openinterp/builder/templates/_agent_role_cells.html +9 -0
  51. openinterp/builder/templates/_causal_chart.html +67 -0
  52. openinterp/builder/templates/_circuit.html +8 -0
  53. openinterp/builder/templates/_dashboard.html +133 -0
  54. openinterp/builder/templates/_experiment.html +500 -0
  55. openinterp/builder/templates/_macros.html +82 -0
  56. openinterp/builder/templates/_patchscope.html +33 -0
  57. openinterp/builder/templates/_prior_art.html +24 -0
  58. openinterp/builder/templates/_specificity.html +21 -0
  59. openinterp/builder/templates/_steering.html +34 -0
  60. openinterp/builder/templates/_superposition.html +62 -0
  61. openinterp/builder/templates/_vocab_chart.html +88 -0
  62. openinterp/builder/templates/_wiring.html +44 -0
  63. openinterp/builder/templates/about.html +95 -0
  64. openinterp/builder/templates/agent.html +69 -0
  65. openinterp/builder/templates/agents.html +80 -0
  66. openinterp/builder/templates/base.html +103 -0
  67. openinterp/builder/templates/circuit.html +64 -0
  68. openinterp/builder/templates/circuits.html +46 -0
  69. openinterp/builder/templates/experiment.html +21 -0
  70. openinterp/builder/templates/hub.html +16 -0
  71. openinterp/builder/templates/index.html +387 -0
  72. openinterp/builder/templates/layer.html +69 -0
  73. openinterp/builder/templates/learned-baseline.html +44 -0
  74. openinterp/builder/templates/model.html +151 -0
  75. openinterp/builder/templates/models.html +47 -0
  76. openinterp/builder/templates/neuron.html +234 -0
  77. openinterp/builder/templates/paper.html +39 -0
  78. openinterp/builder/templates/redirect.html +1 -0
  79. openinterp/builder/templates/research.html +31 -0
  80. openinterp/builder/templates/user.html +54 -0
  81. openinterp/builder/templates/users.html +39 -0
  82. openinterp/builder/templates/wiki/experiment.md +271 -0
  83. openinterp/builder/templates/wiki/index.md +32 -0
  84. openinterp/builder/templates/wiki/neuron.md +30 -0
  85. openinterp/builder/templates/wiki/stop.md +36 -0
  86. openinterp/builder/tree.py +470 -0
  87. openinterp/builder/users.py +142 -0
  88. openinterp/builder/wiki.py +427 -0
  89. openinterp/checks.py +429 -0
  90. openinterp/cli.py +494 -0
  91. openinterp/cli_baseline.py +332 -0
  92. openinterp/cli_benchmark.py +300 -0
  93. openinterp/cli_build.py +128 -0
  94. openinterp/cli_check.py +51 -0
  95. openinterp/cli_concepts.py +76 -0
  96. openinterp/cli_corpus.py +252 -0
  97. openinterp/cli_costs.py +257 -0
  98. openinterp/cli_coverage.py +76 -0
  99. openinterp/cli_crossref.py +86 -0
  100. openinterp/cli_errata.py +142 -0
  101. openinterp/cli_evidence.py +495 -0
  102. openinterp/cli_evidence_store.py +93 -0
  103. openinterp/cli_explain.py +101 -0
  104. openinterp/cli_loop.py +125 -0
  105. openinterp/cli_memory.py +28 -0
  106. openinterp/cli_neuronpedia.py +175 -0
  107. openinterp/cli_pause.py +45 -0
  108. openinterp/cli_pole_effects.py +133 -0
  109. openinterp/cli_probing.py +119 -0
  110. openinterp/cli_registry.py +348 -0
  111. openinterp/cli_reproduce.py +446 -0
  112. openinterp/cli_review.py +302 -0
  113. openinterp/cli_scan.py +223 -0
  114. openinterp/cli_score.py +446 -0
  115. openinterp/cli_sisters.py +86 -0
  116. openinterp/cli_submission.py +217 -0
  117. openinterp/cli_superpositions.py +118 -0
  118. openinterp/cli_wiring.py +54 -0
  119. openinterp/codeid.py +73 -0
  120. openinterp/concepts.py +400 -0
  121. openinterp/core/__init__.py +59 -0
  122. openinterp/core/agent_names.py +76 -0
  123. openinterp/core/attribution.py +101 -0
  124. openinterp/core/cache_dirs.py +15 -0
  125. openinterp/core/causal.py +82 -0
  126. openinterp/core/confounds.py +103 -0
  127. openinterp/core/contributors.py +26 -0
  128. openinterp/core/corpora.py +126 -0
  129. openinterp/core/costs.py +430 -0
  130. openinterp/core/draws.py +109 -0
  131. openinterp/core/errata.py +244 -0
  132. openinterp/core/evidence.py +160 -0
  133. openinterp/core/examples.py +93 -0
  134. openinterp/core/exceptions.py +29 -0
  135. openinterp/core/explanation.py +130 -0
  136. openinterp/core/fence.py +42 -0
  137. openinterp/core/heavy_lock.py +114 -0
  138. openinterp/core/identifiers.py +198 -0
  139. openinterp/core/isolation.py +149 -0
  140. openinterp/core/ledger.py +130 -0
  141. openinterp/core/licenses.py +28 -0
  142. openinterp/core/manifold.py +117 -0
  143. openinterp/core/partners.py +32 -0
  144. openinterp/core/patchscope.py +59 -0
  145. openinterp/core/paths.py +40 -0
  146. openinterp/core/pause.py +96 -0
  147. openinterp/core/pricing.py +178 -0
  148. openinterp/core/prior_art.py +44 -0
  149. openinterp/core/probing.py +146 -0
  150. openinterp/core/profiles.py +221 -0
  151. openinterp/core/project.py +28 -0
  152. openinterp/core/run_environment.py +38 -0
  153. openinterp/core/schemas.py +484 -0
  154. openinterp/core/specificity.py +49 -0
  155. openinterp/core/stats.py +172 -0
  156. openinterp/core/steering.py +49 -0
  157. openinterp/core/strict.py +19 -0
  158. openinterp/core/takedowns.py +136 -0
  159. openinterp/core/transcripts.py +130 -0
  160. openinterp/core/wiring.py +80 -0
  161. openinterp/corpus.py +476 -0
  162. openinterp/corpus_cache.py +145 -0
  163. openinterp/corpus_layouts.py +108 -0
  164. openinterp/corpus_presets.py +168 -0
  165. openinterp/corpus_rebuild.py +93 -0
  166. openinterp/coverage.py +155 -0
  167. openinterp/crossref.py +442 -0
  168. openinterp/data/__init__.py +9 -0
  169. openinterp/data/offensive_words.txt +58 -0
  170. openinterp/data/public_suffix_list.dat +15771 -0
  171. openinterp/data/sample_corpus.txt +40 -0
  172. openinterp/draw_steps.py +359 -0
  173. openinterp/errata_checks.py +129 -0
  174. openinterp/evidence.py +462 -0
  175. openinterp/evidence_branch.py +119 -0
  176. openinterp/evidence_lenses.py +75 -0
  177. openinterp/experiment_loop.py +357 -0
  178. openinterp/explain.py +252 -0
  179. openinterp/half_precision.py +79 -0
  180. openinterp/harness/__init__.py +37 -0
  181. openinterp/harness/ane/__init__.py +9 -0
  182. openinterp/harness/ane/backend.py +324 -0
  183. openinterp/harness/ane/cache.py +276 -0
  184. openinterp/harness/ane/chunks.py +163 -0
  185. openinterp/harness/ane/convert.py +116 -0
  186. openinterp/harness/ane/forward.py +258 -0
  187. openinterp/harness/ane/qualified.py +40 -0
  188. openinterp/harness/annotations.py +329 -0
  189. openinterp/harness/attribution.py +96 -0
  190. openinterp/harness/causal.py +264 -0
  191. openinterp/harness/confounds.py +184 -0
  192. openinterp/harness/covariance.py +301 -0
  193. openinterp/harness/crossref.py +195 -0
  194. openinterp/harness/cuda_devices.py +88 -0
  195. openinterp/harness/dead.py +124 -0
  196. openinterp/harness/devices.py +401 -0
  197. openinterp/harness/dictionaries.py +496 -0
  198. openinterp/harness/dreaming.py +116 -0
  199. openinterp/harness/environment.py +64 -0
  200. openinterp/harness/explanation_scores.py +371 -0
  201. openinterp/harness/features.py +276 -0
  202. openinterp/harness/first_position.py +137 -0
  203. openinterp/harness/heads.py +226 -0
  204. openinterp/harness/hooks.py +499 -0
  205. openinterp/harness/input_attribution.py +367 -0
  206. openinterp/harness/jlens.py +183 -0
  207. openinterp/harness/layer_cache.py +301 -0
  208. openinterp/harness/layer_major.py +274 -0
  209. openinterp/harness/lookahead.py +93 -0
  210. openinterp/harness/manifold.py +200 -0
  211. openinterp/harness/memory.py +471 -0
  212. openinterp/harness/moe.py +134 -0
  213. openinterp/harness/patcher.py +322 -0
  214. openinterp/harness/patchscope.py +177 -0
  215. openinterp/harness/probing.py +383 -0
  216. openinterp/harness/scan.py +462 -0
  217. openinterp/harness/scan_backend.py +201 -0
  218. openinterp/harness/sister_passes.py +463 -0
  219. openinterp/harness/sisters.py +363 -0
  220. openinterp/harness/sites.py +101 -0
  221. openinterp/harness/specificity.py +107 -0
  222. openinterp/harness/steering.py +159 -0
  223. openinterp/harness/superpositions.py +116 -0
  224. openinterp/harness/units.py +203 -0
  225. openinterp/harness/wiring.py +432 -0
  226. openinterp/harness/wiring_activations.py +285 -0
  227. openinterp/langid.py +73 -0
  228. openinterp/names.py +129 -0
  229. openinterp/neuronpedia_baseline.py +88 -0
  230. openinterp/neuronpedia_review.py +148 -0
  231. openinterp/planner.py +466 -0
  232. openinterp/planner_effects.py +89 -0
  233. openinterp/planner_heads.py +92 -0
  234. openinterp/planner_models.py +174 -0
  235. openinterp/planner_sisters.py +122 -0
  236. openinterp/planner_states.py +57 -0
  237. openinterp/planner_superpositions.py +114 -0
  238. openinterp/pole_effects.py +102 -0
  239. openinterp/prior_art.py +143 -0
  240. openinterp/reproduce.py +450 -0
  241. openinterp/revision.py +45 -0
  242. openinterp/runner_cache.py +35 -0
  243. openinterp/score_controls.py +169 -0
  244. openinterp/sensitive.py +243 -0
  245. openinterp/show.py +236 -0
  246. openinterp/signing.py +209 -0
  247. openinterp/sisters.py +476 -0
  248. openinterp/superposition_finders.py +225 -0
  249. openinterp/sync/__init__.py +5 -0
  250. openinterp/sync/r2.py +280 -0
  251. openinterp/syntax.py +399 -0
  252. openinterp/takedown_checks.py +76 -0
  253. openinterp/toxicity.py +158 -0
  254. openinterp/verdict_regression.py +79 -0
  255. openinterp/wiring_backfill.py +170 -0
  256. openinterp/wordnet.py +362 -0
@@ -0,0 +1,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()