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,462 @@
1
+ """One pass over a corpus that gathers everything an audit needs about a neuron pole.
2
+
3
+ For every non-padding token, it records three things:
4
+
5
+ - the pole strength, for the top examples and histogram;
6
+ - the whole layer's activations, for partner neuron poles (co-activation lift, Spearman,
7
+ Pearson);
8
+ - the token id and position, for the confound profile.
9
+
10
+ Documents are scanned whole by default. With ``window_tokens``, each is cut into
11
+ overlapping windows that start every ``stride`` tokens. So a long document is read in
12
+ full without one huge forward pass. Every token still counts once in the statistics. It
13
+ counts in the first window after the first that reaches it. There it has at least
14
+ ``window_tokens - stride`` tokens of context (the strided-perplexity convention).
15
+ A unit with a first-token floor (protocol 0.8) is above its line at the first token,
16
+ whatever the token. So its scan leaves first tokens out of its scale, histogram and
17
+ top-example ranking (:func:`settle`). That is decided after the pass, so every scan
18
+ keeps whether each counted token is its piece's first.
19
+ Top examples are windows, decoded back to text so a record re-measures them as the
20
+ scan did, and no two overlap.
21
+
22
+ Each forward pass yields a :class:`Chunk`: the whole layer's activations on every real
23
+ token of its pieces. Given a :class:`~openinterp.harness.layer_cache.LayerCache`, a scan
24
+ stores its chunks, and a later scan of any neuron in that layer replays them.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from collections.abc import Callable, Iterator, Sequence
30
+ from dataclasses import dataclass, field
31
+ from typing import TYPE_CHECKING
32
+
33
+ import torch
34
+
35
+ from openinterp.core.schemas import (
36
+ ActivatingExample,
37
+ ConfoundProfile,
38
+ CorrelatedNeuron,
39
+ Pole,
40
+ ReviewPolicy,
41
+ )
42
+ from openinterp.harness.annotations import LabelPass, Tagger, TokenLabels
43
+ from openinterp.harness.confounds import ConfoundAccumulator
44
+ from openinterp.harness.covariance import PartnerSearch
45
+ from openinterp.harness.hooks import Inputs, NeuronHookRunner, pole_strength
46
+ from openinterp.harness.scan_backend import forward_chunks, rescore_scan, with_ane_fallback
47
+
48
+ if TYPE_CHECKING:
49
+ from openinterp.harness.layer_cache import LayerCache
50
+
51
+
52
+ # Padded tokens per forward pass (pieces × the longest). Part of a cached scan's key, as
53
+ # it sets padding. On Qwen2.5-0.5B over corpus.jsonl, the model reads 27% fewer tokens
54
+ # (padding included) than with 8 pieces a pass. 2048 is near the fastest budget on
55
+ # Apple's GPU.
56
+ BATCH_TOKENS = 2048
57
+
58
+
59
+ @dataclass
60
+ class ScanResult:
61
+ """What one scan found: top examples, every counted strength, partners and confounds."""
62
+
63
+ examples: list[ActivatingExample]
64
+ values: list[float] # every scanned token's pole strength, each token once
65
+ correlated: list[CorrelatedNeuron]
66
+ confounds: ConfoundProfile
67
+ windows: int = 0 # forward-pass inputs: documents, or windows of them
68
+ opposite: OppositeScan | None = None # the neuron's other pole, from the same pass
69
+ # Protocol 0.8: first tokens are left out of examples and values (see ``settle``).
70
+ first_token_floor: bool = False
71
+
72
+
73
+ @dataclass
74
+ class OppositeScan:
75
+ """The neuron's other pole, from the same pass: its top examples and strengths."""
76
+
77
+ pole: Pole
78
+ examples: list[ActivatingExample]
79
+ values: list[float]
80
+ first_token_floor: bool = False
81
+
82
+
83
+ Strengths = Callable[[Inputs], torch.Tensor]
84
+
85
+
86
+ @torch.no_grad()
87
+ def first_position_share(
88
+ runner: NeuronHookRunner, strengths: Strengths, line: float, batch_size: int = 8192
89
+ ) -> float:
90
+ """Return the share of the vocabulary on which ``strengths`` is at or above ``line``.
91
+
92
+ This is at a text's first position, with special tokens left out. Attention there
93
+ sees only the token itself. So this is a property of the weights, the same for every
94
+ corpus.
95
+ """
96
+ from openinterp.harness import first_position
97
+
98
+ if reader := getattr(strengths, "first_position", None): # a neuron's: computed once
99
+ values = reader()
100
+ else:
101
+ values = first_position.strengths_of(runner, strengths, batch_size)
102
+ return float((values >= line).sum()) / max(len(values), 1)
103
+
104
+
105
+ def has_first_token_floor(
106
+ runner: NeuronHookRunner, strengths: Strengths, scale: float, policy: ReviewPolicy
107
+ ) -> bool:
108
+ """Return whether the unit has a first-token floor (protocol 0.8).
109
+
110
+ It has one when it is active at the first position on at least
111
+ ``policy.max_active_share`` of the vocabulary. Its line comes from ``scale``.
112
+ """
113
+ if scale <= 0:
114
+ return False
115
+ line = policy.activation_threshold * scale
116
+ return first_position_share(runner, strengths, line) >= policy.max_active_share
117
+
118
+
119
+ def settle(
120
+ runner: NeuronHookRunner,
121
+ strengths: Strengths,
122
+ values: Sequence[float],
123
+ first: Sequence[bool],
124
+ top: TopExamples,
125
+ k: int,
126
+ labels: TokenLabels | None = None,
127
+ ) -> tuple[list[ActivatingExample], list[float], bool]:
128
+ """Return a scan's top examples, counted values, and whether it has a first-token floor.
129
+
130
+ The floor is judged against the scale that the unit would have without first tokens.
131
+ That is the top example's peak after its first token. It is the scale that the unit
132
+ records if it has the floor. With the floor, first tokens are left out of the
133
+ examples' ranking and of the values. Each example carries its peak token's ``labels``.
134
+ """
135
+ best = top.top(runner.tokenizer, 1, skip_first=True)
136
+ scale = best[0].max_activation if best else 0.0
137
+ floor = has_first_token_floor(runner, strengths, scale, ReviewPolicy())
138
+ kept = [v for v, f in zip(values, first, strict=True) if not f] if floor else list(values)
139
+ return top.top(runner.tokenizer, k, skip_first=floor, labels=labels), kept, floor
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class Piece:
144
+ """What one forward pass reads: a whole document, or tokens [start, end) of it."""
145
+
146
+ doc: int
147
+ start: int
148
+ end: int
149
+ text: str
150
+ fresh: int | None = None # trailing tokens new to the scan; None: every token is
151
+
152
+ def overlaps(self, other: Piece) -> bool:
153
+ """True when the two pieces share tokens of one document."""
154
+ return self.doc == other.doc and self.start < other.end and other.start < self.end
155
+
156
+
157
+ def default_stride(window_tokens: int) -> int:
158
+ """Half a window, so every counted token has at least half a window of context."""
159
+ return max(window_tokens // 2, 1)
160
+
161
+
162
+ def cut_pieces(
163
+ tokenizer, texts: Sequence[str], window_tokens: int | None, stride: int | None
164
+ ) -> Iterator[Piece]:
165
+ """Whole documents, or each document's windows (see the module docstring)."""
166
+ if window_tokens is None:
167
+ yield from (Piece(doc, 0, 1, text) for doc, text in enumerate(texts))
168
+ return
169
+ stride = stride or default_stride(window_tokens)
170
+ if not 0 < stride <= window_tokens:
171
+ raise ValueError(f"stride must be in 1..{window_tokens}, got {stride}")
172
+ for doc, text in enumerate(texts):
173
+ ids = tokenizer.encode(text, add_special_tokens=False)
174
+ start = prev_end = 0
175
+ while start < len(ids):
176
+ end = min(start + window_tokens, len(ids))
177
+ fresh = None if start == 0 else end - prev_end
178
+ yield Piece(doc, start, end, tokenizer.decode(ids[start:end]), fresh)
179
+ if end == len(ids):
180
+ break
181
+ start, prev_end = start + stride, end
182
+
183
+
184
+ @dataclass
185
+ class Batch:
186
+ """One forward pass of pieces, with which of its tokens the scan counts."""
187
+
188
+ pieces: list[Piece]
189
+ encoded: dict[str, torch.Tensor]
190
+ ids: torch.Tensor # [b, seq], on the CPU
191
+ mask: torch.Tensor # real (non-padding) tokens
192
+ counted: torch.Tensor # real tokens new to the scan
193
+ positions: torch.Tensor
194
+
195
+ @property
196
+ def first(self) -> torch.Tensor:
197
+ """Each piece's first real token."""
198
+ return self.mask & (self.mask.cumsum(1) == 1)
199
+
200
+
201
+ def plan_batches(lengths: Sequence[int], batch_tokens: int) -> list[int]:
202
+ """Return how many consecutive pieces each forward pass takes.
203
+
204
+ That is as many as fit in ``batch_tokens`` once padded to the longest, and at least one.
205
+ """
206
+ sizes: list[int] = []
207
+ size = longest = 0
208
+ for n in lengths:
209
+ if size and max(longest, n) * (size + 1) > batch_tokens:
210
+ sizes.append(size)
211
+ size = longest = 0
212
+ size, longest = size + 1, max(longest, n)
213
+ return [*sizes, size] if size else sizes
214
+
215
+
216
+ def batches(
217
+ runner: NeuronHookRunner, pieces: Sequence[Piece], batch_tokens: int = BATCH_TOKENS
218
+ ) -> Iterator[Batch]:
219
+ """``pieces`` in order, in forward passes of at most ``batch_tokens`` padded tokens."""
220
+ if not pieces:
221
+ return
222
+ every = runner.tokenizer([p.text for p in pieces], return_tensors="pt", padding=True)
223
+ lengths = every.get("attention_mask", torch.ones_like(every["input_ids"])).sum(1).tolist()
224
+ start = 0
225
+ for size in plan_batches(lengths, batch_tokens):
226
+ batch = list(pieces[start : start + size])
227
+ start += size
228
+ encoded = runner.encode([p.text for p in batch])
229
+ ids = encoded["input_ids"].cpu()
230
+ mask = encoded.get("attention_mask", torch.ones_like(encoded["input_ids"])).bool().cpu()
231
+ # Count only each piece's fresh tail: real tokens at or after it ≤ fresh.
232
+ fresh = torch.tensor([p.fresh or ids.shape[1] for p in batch]).unsqueeze(1)
233
+ counted = mask & (mask.flip(1).cumsum(1).flip(1) <= fresh)
234
+ positions = torch.arange(ids.shape[1]).expand_as(ids)
235
+ yield Batch(batch, encoded, ids, mask, counted, positions)
236
+
237
+
238
+ @dataclass
239
+ class Chunk:
240
+ """One forward pass, flattened to its pieces' real tokens in scan order."""
241
+
242
+ pieces: list[Piece]
243
+ lengths: list[int] # real tokens of each piece
244
+ ids: torch.Tensor # [n]
245
+ positions: torch.Tensor # [n]
246
+ counted: torch.Tensor # [n] bool: tokens new to the scan
247
+ acts: torch.Tensor # [n, width] the whole layer, in the model's dtype, on the CPU
248
+
249
+ @property
250
+ def first(self) -> torch.Tensor:
251
+ """``[n]`` bool: each piece's first token (pieces are padded on the right)."""
252
+ return self.positions == 0
253
+
254
+ def rows(self) -> Iterator[tuple[Piece, slice]]:
255
+ """Each piece and its rows."""
256
+ start = 0
257
+ for piece, n in zip(self.pieces, self.lengths, strict=True):
258
+ yield piece, slice(start, start + n)
259
+ start += n
260
+
261
+
262
+ def layer_chunks(
263
+ runner: NeuronHookRunner, pieces: Sequence[Piece], layer: int, batch_tokens: int
264
+ ) -> Iterator[Chunk]:
265
+ """Run the model over ``pieces`` and yield ``layer``'s activations batch by batch."""
266
+ for batch in batches(runner, pieces, batch_tokens):
267
+ cache = runner.layer_activations(batch.encoded, [layer])
268
+ yield Chunk(
269
+ pieces=batch.pieces,
270
+ lengths=batch.mask.sum(1).tolist(),
271
+ ids=batch.ids[batch.mask],
272
+ positions=batch.positions[batch.mask],
273
+ counted=batch.counted[batch.mask],
274
+ acts=cache[layer].cpu()[batch.mask],
275
+ )
276
+
277
+
278
+ def scan_chunks(
279
+ runner: NeuronHookRunner,
280
+ pieces: Sequence[Piece],
281
+ layer: int,
282
+ batch_tokens: int = BATCH_TOKENS,
283
+ cache: LayerCache | None = None,
284
+ ) -> Iterator[Chunk]:
285
+ """Yield ``layer``'s chunks, replayed from ``cache`` if it holds them.
286
+
287
+ Otherwise they come from the model, and are stored in ``cache``.
288
+ """
289
+ chunks = forward_chunks(runner, pieces, layer, batch_tokens) # on the ANE if it can
290
+ return chunks if cache is None else cache.through(runner, layer, pieces, batch_tokens, chunks)
291
+
292
+
293
+ class TopExamples:
294
+ """Collects every piece's strengths and keeps the ``k`` strongest that don't overlap."""
295
+
296
+ def __init__(self) -> None:
297
+ self.candidates: list[tuple[Piece, list[int], list[float]]] = []
298
+
299
+ def update(self, batch: Batch, strengths: torch.Tensor) -> None:
300
+ """Add a batch's pieces as candidates, with their real tokens' strengths."""
301
+ for row, piece in enumerate(batch.pieces):
302
+ mask = batch.mask[row]
303
+ self.add(piece, batch.ids[row][mask].tolist(), strengths[row][mask].tolist())
304
+
305
+ def update_chunk(self, chunk: Chunk, strengths: torch.Tensor) -> None:
306
+ """Add a chunk's pieces as candidates, with their strengths."""
307
+ for piece, rows in chunk.rows():
308
+ self.add(piece, chunk.ids[rows].tolist(), strengths[rows].tolist())
309
+
310
+ def add(self, piece: Piece, ids: list[int], values: list[float]) -> None:
311
+ """Add one piece as a candidate, unless it has no strengths."""
312
+ if values:
313
+ self.candidates.append((piece, ids, values))
314
+
315
+ def top(
316
+ self, tokenizer, k: int, skip_first: bool = False, labels: TokenLabels | None = None
317
+ ) -> list[ActivatingExample]:
318
+ """Return the ``k`` strongest pieces by their peak.
319
+
320
+ With ``skip_first``, the peak is after the first token. Each has its peak token's
321
+ ``labels``.
322
+ """
323
+ start = int(skip_first)
324
+ ranked = [(max(v[start:]), p, i, v) for p, i, v in self.candidates if len(v) > start]
325
+ examples: list[ActivatingExample] = []
326
+ chosen: list[Piece] = []
327
+ for peak_value, piece, row_ids, row_vals in sorted(ranked, key=lambda c: -c[0]):
328
+ if len(examples) == k:
329
+ break
330
+ if any(piece.overlaps(c) for c in chosen):
331
+ continue
332
+ chosen.append(piece)
333
+ peak = row_vals.index(peak_value, start)
334
+ examples.append(
335
+ ActivatingExample(
336
+ text=piece.text,
337
+ tokens=[tokenizer.decode([i]) for i in row_ids],
338
+ activations=[round(v, 5) for v in row_vals],
339
+ max_activation=peak_value,
340
+ max_token_index=peak,
341
+ peak_labels=labels.at(piece, row_ids, peak) if labels else None,
342
+ )
343
+ )
344
+ return examples
345
+
346
+
347
+ def scan_neuron(
348
+ runner: NeuronHookRunner,
349
+ texts: Sequence[str],
350
+ layer: int,
351
+ neuron_idx: int,
352
+ pole: Pole,
353
+ *,
354
+ k: int = 20,
355
+ batch_tokens: int = BATCH_TOKENS,
356
+ n_correlated: int = 10,
357
+ min_count: int = 3,
358
+ window_tokens: int | None = None,
359
+ stride: int | None = None,
360
+ cache: LayerCache | None = None,
361
+ opposite_k: int = 5,
362
+ taggers: Sequence[Tagger] = (),
363
+ ) -> ScanResult:
364
+ """Scan ``texts`` for one neuron pole, and for its other pole.
365
+
366
+ The other pole gets its top ``opposite_k`` examples and histogram. That is free,
367
+ because the pass reads the whole activation. With ``cache``, replay the layer's stored
368
+ activations if it holds this scan, and store them if not. Each of ``taggers`` labels
369
+ the counted tokens, and its confound joins the profile's.
370
+ """
371
+ if runner.tokenizer is None:
372
+ raise ValueError("scan_neuron requires a tokenizer")
373
+ other: Pole = "negative" if pole == "positive" else "positive"
374
+ pieces = list(cut_pieces(runner.tokenizer, texts, window_tokens, stride))
375
+
376
+ def run(): # fresh accumulators, so a pass redone on torch starts from nothing
377
+ labels = LabelPass(runner.tokenizer, texts, taggers)
378
+ poles = (pole, other)
379
+ return labels, _pole_pass(
380
+ runner, pieces, layer, neuron_idx, poles, batch_tokens, cache, labels
381
+ )
382
+
383
+ labels, scan = with_ane_fallback(runner, run)
384
+ tops = (scan.top, scan.other_top)
385
+ rescore_scan(
386
+ runner, pieces, layer, neuron_idx, (pole, other), tops, (k, opposite_k), batch_tokens
387
+ )
388
+ read, read_other = (_reader(runner, layer, neuron_idx, p) for p in (pole, other))
389
+ tokens = labels.tokens
390
+ examples, values, floor = settle(runner, read, scan.values, scan.first, scan.top, k, tokens)
391
+ other_examples, other_values, other_floor = settle(
392
+ runner, read_other, scan.other_values, scan.first, scan.other_top, opposite_k, tokens
393
+ )
394
+ # "Active" for co-activation uses the protocol's own definition for the target.
395
+ active = ReviewPolicy().activation_threshold * max(values, default=0.0)
396
+ confounds = labels.finalize(scan.confounds.finalize(runner.tokenizer, min_count), values)
397
+ return ScanResult(
398
+ examples=examples,
399
+ values=values,
400
+ correlated=scan.partners.partners(neuron_idx, active, k=n_correlated) if active > 0 else [],
401
+ confounds=confounds,
402
+ windows=len(pieces),
403
+ opposite=OppositeScan(other, other_examples, other_values, other_floor),
404
+ first_token_floor=floor,
405
+ )
406
+
407
+
408
+ @dataclass
409
+ class _PolePass:
410
+ """What one pass over the pieces gathers for a neuron's pole and its other pole."""
411
+
412
+ partners: PartnerSearch
413
+ confounds: ConfoundAccumulator = field(default_factory=ConfoundAccumulator)
414
+ top: TopExamples = field(default_factory=TopExamples)
415
+ other_top: TopExamples = field(default_factory=TopExamples)
416
+ values: list[float] = field(default_factory=list)
417
+ other_values: list[float] = field(default_factory=list)
418
+ first: list[bool] = field(default_factory=list)
419
+
420
+
421
+ def _pole_pass(runner, pieces, layer: int, neuron_idx: int, poles, batch_tokens, cache, labels):
422
+ pole, other = poles
423
+ scan = _PolePass(PartnerSearch(runner.mlp_width(layer)))
424
+ for chunk in scan_chunks(runner, pieces, layer, batch_tokens, cache):
425
+ acts = chunk.acts.float() # [n, d_mlp]
426
+ strengths = pole_strength(acts[:, neuron_idx], pole)
427
+ counted = chunk.counted
428
+ scan.partners.update(strengths[counted], acts[counted])
429
+ scan.confounds.update(chunk.ids[counted], chunk.positions[counted], strengths[counted])
430
+ labels.update_chunk(chunk, strengths)
431
+ scan.values.extend(strengths[counted].tolist())
432
+ scan.first.extend(chunk.first[counted].tolist())
433
+ scan.top.update_chunk(chunk, strengths)
434
+ other_strengths = pole_strength(acts[:, neuron_idx], other)
435
+ scan.other_values.extend(other_strengths[counted].tolist())
436
+ scan.other_top.update_chunk(chunk, other_strengths)
437
+ return scan
438
+
439
+
440
+ def _reader(runner, layer: int, neuron_idx: int, pole: Pole) -> Strengths:
441
+ from openinterp.harness.first_position import NeuronReader
442
+
443
+ return NeuronReader(runner, layer, neuron_idx, pole)
444
+
445
+
446
+ def top_activating_examples(
447
+ runner: NeuronHookRunner,
448
+ texts: Sequence[str],
449
+ layer: int,
450
+ neuron_idx: int,
451
+ pole: Pole,
452
+ *,
453
+ k: int = 20,
454
+ batch_tokens: int = BATCH_TOKENS,
455
+ ) -> tuple[list[ActivatingExample], list[float]]:
456
+ """Scan ``texts``; return the ``k`` strongest snippets and every token's pole strength.
457
+
458
+ A thin wrapper over :func:`scan_neuron`, which also gathers correlated neurons and
459
+ the confound profile.
460
+ """
461
+ result = scan_neuron(runner, texts, layer, neuron_idx, pole, k=k, batch_tokens=batch_tokens)
462
+ return result.examples, result.values
@@ -0,0 +1,201 @@
1
+ """Where a scan's forward pass runs, and the exact measurement of its top candidates.
2
+
3
+ A runner can carry a ``scan_backend``, an :class:`~openinterp.harness.ane.backend.AneScan`.
4
+ It runs the scan's forward pass on Apple's Neural Engine (ANE). The device planner
5
+ (``harness/devices.py``) adds it when the GPU is busy. The ANE computes in fp16, about 1%
6
+ from fp32. So after the pass, :func:`rescore_scan` measures each pole's strongest
7
+ candidates again on the runner's own device:
8
+
9
+ - the :data:`RESCORE_TOP` strongest pieces;
10
+ - and any piece within :data:`RESCORE_MARGIN` of the 20th strongest, up to
11
+ :data:`RESCORE_CAP` pieces.
12
+
13
+ On ``corpus.jsonl`` that rule found the exact top 20 for every neuron and both poles. The
14
+ layers were GPT-2 L5, Qwen2.5-0.5B L12 and Qwen2.5-1.5B L8 (docs/analyses/ane-feasibility.md).
15
+ Then it makes sure that every example the scan will keep was measured again. The
16
+ numbers that ``open-interpretability reproduce`` checks are then the device's numbers. The
17
+ histogram, partners and confounds keep the ANE's values, which correlate with fp32 at
18
+ r ≥ 0.9998 for every neuron.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from collections.abc import Callable, Iterator, Sequence
24
+ from typing import TYPE_CHECKING
25
+
26
+ import click
27
+ import torch
28
+
29
+ from openinterp.harness.hooks import pole_strength
30
+
31
+ if TYPE_CHECKING:
32
+ from openinterp.core.schemas import Pole
33
+ from openinterp.harness.hooks import NeuronHookRunner
34
+ from openinterp.harness.scan import Chunk, Piece, TopExamples
35
+
36
+ RESCORE_TOP = 60
37
+ RESCORE_MARGIN = 0.06
38
+ RESCORE_CAP = 120
39
+ RESCORE_RANK = 20 # the margin is below this rank's peak
40
+ RESCORE_BATCH = 16 # pieces per forward pass when measuring again
41
+ RESCORE_ROUNDS = 5 # rounds that add examples the selection would keep unmeasured
42
+
43
+
44
+ def ane_scan(runner: NeuronHookRunner, pieces: Sequence[Piece], layer: int, batch_tokens: int):
45
+ """The runner's ANE backend, made ready for this scan, if it can run it; else None.
46
+
47
+ If it can't, it says why on stderr, once for each reason.
48
+ """
49
+ backend = getattr(runner, "scan_backend", None)
50
+ if backend is None or not pieces:
51
+ return None
52
+ every = runner.tokenizer([p.text for p in pieces], return_tensors="pt", padding=True)
53
+ mask = every.get("attention_mask", torch.ones_like(every["input_ids"]))
54
+ reason = backend.prepare(mask.sum(1).tolist(), layer, batch_tokens)
55
+ if reason is None:
56
+ return backend
57
+ if reason not in backend.noted:
58
+ backend.noted.add(reason)
59
+ click.echo(f"This scan runs on {runner.device.type}, not the ANE: {reason}.", err=True)
60
+ return None
61
+
62
+
63
+ class AneError(RuntimeError):
64
+ """The ANE failed during a scan. The scan is then done again on torch."""
65
+
66
+
67
+ def with_ane_fallback(runner: NeuronHookRunner, run: Callable):
68
+ """``run()``; if the ANE fails during it, stop using the ANE and ``run()`` again.
69
+
70
+ The second run starts from nothing, so no partial result of the ANE is kept.
71
+ """
72
+ try:
73
+ return run()
74
+ except AneError as exc:
75
+ backend = runner.scan_backend
76
+ backend.broken = f"it failed during a scan ({exc.__cause__ or exc})"
77
+ click.echo(
78
+ f"The ANE failed, so this scan runs again on {runner.device.type}: {exc}", err=True
79
+ )
80
+ return run()
81
+
82
+
83
+ def scan_device(runner: NeuronHookRunner, pieces: Sequence[Piece], layer, batch_tokens) -> str:
84
+ """The device a scan of ``pieces`` runs on: the backend's name, or the runner's device."""
85
+ backend = ane_scan(runner, pieces, layer, batch_tokens)
86
+ return backend.name if backend is not None else runner.device.type
87
+
88
+
89
+ def forward_chunks(
90
+ runner: NeuronHookRunner, pieces: Sequence[Piece], layer: int, batch_tokens: int
91
+ ) -> Iterator[Chunk]:
92
+ """``layer``'s chunks over ``pieces``, from the ANE if it can run them, else torch."""
93
+ from openinterp.harness.scan import layer_chunks
94
+
95
+ backend = ane_scan(runner, pieces, layer, batch_tokens)
96
+ if backend is None:
97
+ return layer_chunks(runner, pieces, layer, batch_tokens)
98
+ backend.used = True
99
+ return backend.chunks(runner, pieces, layer, batch_tokens)
100
+
101
+
102
+ def rescore_scan(
103
+ runner, pieces, layer: int, neuron_idx: int, poles, tops, ks, batch_tokens
104
+ ) -> None:
105
+ """Measure again the top candidates of each pole, if the scan's values are the ANE's.
106
+
107
+ ``poles``, ``tops`` and ``ks`` are each pole, its :class:`TopExamples` and the number
108
+ of examples it keeps.
109
+ """
110
+ if ane_scan(runner, pieces, layer, batch_tokens) is None:
111
+ return
112
+ for pole, top, k in zip(poles, tops, ks, strict=True):
113
+ rescore(top, _measure(runner, layer, neuron_idx, pole), k)
114
+
115
+
116
+ def _measure(runner, layer: int, neuron_idx: int, pole: Pole) -> Callable:
117
+ """A function that measures the pole's strengths on texts, :data:`RESCORE_BATCH` a pass.
118
+
119
+ On a model that streams, the passes run layer-major (``harness/layer_major.py``), so
120
+ each layer is read from disk once for all of them.
121
+ """
122
+ from openinterp.harness.layer_major import map_layer_major, prompt_tokens
123
+
124
+ def one(texts: list[str]) -> list[list[float]]:
125
+ encoded = runner.encode(texts)
126
+ mask = encoded.get("attention_mask", torch.ones_like(encoded["input_ids"])).bool()
127
+ with torch.no_grad():
128
+ acts = runner.layer_activations(encoded, [layer])[layer][..., neuron_idx]
129
+ values = pole_strength(acts.float(), pole).cpu()
130
+ return [values[r][mask[r].cpu()].tolist() for r in range(len(texts))]
131
+
132
+ def measure(texts: list[str]) -> list[list[float]]:
133
+ parts = [texts[i : i + RESCORE_BATCH] for i in range(0, len(texts), RESCORE_BATCH)]
134
+ tokens = RESCORE_BATCH * prompt_tokens(runner, texts)
135
+ done = map_layer_major(runner, one, parts, tokens=tokens, logits=False)
136
+ return [values for part in done for values in part]
137
+
138
+ return measure
139
+
140
+
141
+ def rescore(top: TopExamples, measure: Callable[[list[str]], list[list[float]]], k: int) -> int:
142
+ """Replace the values of ``top``'s strongest candidates with ``measure``'s.
143
+
144
+ Return how many candidates it measured again.
145
+ """
146
+ done: set[int] = set()
147
+ todo = chosen(top.candidates)
148
+ for _ in range(RESCORE_ROUNDS):
149
+ todo -= done
150
+ if not todo:
151
+ break
152
+ order = sorted(todo)
153
+ new = measure([top.candidates[i][0].text for i in order]) # all of them at once
154
+ for i, values in zip(order, new, strict=True):
155
+ piece, ids, old = top.candidates[i]
156
+ if len(values) != len(old):
157
+ raise ValueError(f"measured {len(values)} tokens of a {len(old)}-token piece")
158
+ top.candidates[i] = (piece, ids, values)
159
+ done |= todo
160
+ todo = kept(top.candidates, k)
161
+ return len(done)
162
+
163
+
164
+ def chosen(candidates: list) -> set[int]:
165
+ """Indices of the candidates to measure again, by their peak with and without token 0."""
166
+ picked: set[int] = set()
167
+ for start in (0, 1):
168
+ peaks = torch.tensor(
169
+ [max(v[start:]) if len(v) > start else -torch.inf for *_, v in candidates]
170
+ )
171
+ order = peaks.argsort(descending=True)
172
+ ranked = peaks[order]
173
+ if not len(ranked):
174
+ continue
175
+ nth = ranked[min(RESCORE_RANK, len(ranked)) - 1]
176
+ line = nth - RESCORE_MARGIN * nth.abs() - 1e-3 * ranked[0].abs()
177
+ for rank, index in enumerate(order.tolist()):
178
+ if rank < RESCORE_TOP or (ranked[rank] >= line and rank < RESCORE_CAP):
179
+ picked.add(index)
180
+ return picked
181
+
182
+
183
+ def kept(candidates: list, k: int) -> set[int]:
184
+ """Indices of the candidates that the top ``k`` would keep, with and without token 0.
185
+
186
+ As ``TopExamples.top``, it skips a piece that overlaps one it kept.
187
+ """
188
+ keep: set[int] = set()
189
+ for start in (0, 1):
190
+ ranked = sorted(
191
+ (i for i, (*_, v) in enumerate(candidates) if len(v) > start),
192
+ key=lambda i: -max(candidates[i][2][start:]),
193
+ )
194
+ taken: list[int] = []
195
+ for i in ranked:
196
+ if len(taken) == k:
197
+ break
198
+ if not any(candidates[i][0].overlaps(candidates[j][0]) for j in taken):
199
+ taken.append(i)
200
+ keep.update(taken)
201
+ return keep