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,301 @@
1
+ """On-disk cache of a scan's whole-layer activations.
2
+
3
+ One forward pass over the corpus captures every neuron of a layer, so a scan stores
4
+ all of them. Later audits of any neuron in that layer, and per-token analysis
5
+ (``open-interpretability activations``), replay the stored tokens instead of re-running the model.
6
+
7
+ An entry is keyed by everything that sets its numbers. That is the model, its revision,
8
+ the weights' dtype and the device type (``coreml-ane`` for a scan on the ANE). It is also
9
+ the torch and transformers versions and the layer. Last, it is the token budget per
10
+ batch (which sets padding), and the exact pieces scanned (texts and windows). Replaying
11
+ an entry gives the same scan result as running the model, bit for bit.
12
+
13
+ Each entry is a directory ``<root>/<model>/L<layer>/<key>/``. It holds ``meta.json``,
14
+ ``pieces.json`` and ``acts.bin`` (``[rows, width]`` in the activation dtype). It also
15
+ holds the per-row ``ids.npy``, ``positions.npy``, ``piece.npy`` and ``counted.npy``.
16
+ The rows are every real (non-padding) token of every piece, in scan order. ``meta.json``
17
+ lists how many pieces each forward pass took. Entries are written to a temporary
18
+ directory and renamed into place, so concurrent scans never see a partial one. Once the
19
+ cache outgrows its size cap, the least recently used entries go.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ import json
26
+ import os
27
+ import secrets
28
+ import shutil
29
+ from collections.abc import Iterator, Sequence
30
+ from contextlib import suppress
31
+ from dataclasses import asdict, dataclass
32
+ from datetime import UTC, datetime
33
+ from pathlib import Path
34
+
35
+ import numpy as np
36
+ import torch
37
+
38
+ from openinterp.core.cache_dirs import cache_root
39
+ from openinterp.harness.hooks import NeuronHookRunner
40
+ from openinterp.harness.scan import Chunk, Piece
41
+ from openinterp.harness.scan_backend import scan_device
42
+
43
+ FORMAT = 2 # 2: batches by token budget, recorded in meta.json
44
+ ENV = "OPENINTERP_SCAN_CACHE" # cache directory, or "off"
45
+ SIZE_ENV = "OPENINTERP_SCAN_CACHE_GB"
46
+ DEFAULT_GB = 40.0
47
+
48
+ # numpy has no bfloat16: store its bits as int16 and view them back.
49
+ _BITS = {torch.bfloat16: torch.int16}
50
+
51
+
52
+ def default_root() -> Path | None:
53
+ """``$OPENINTERP_SCAN_CACHE``, else ``$XDG_CACHE_HOME/openinterp/scans``; None if off."""
54
+ return cache_root(ENV, "scans")
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class Entry:
59
+ """One cached layer scan."""
60
+
61
+ path: Path
62
+ meta: dict
63
+
64
+ @property
65
+ def size(self) -> int:
66
+ """The entry's size on disk, in bytes."""
67
+ return sum(f.stat().st_size for f in self.path.iterdir())
68
+
69
+ @property
70
+ def last_used(self) -> float:
71
+ """When the entry was last read or written, as a timestamp."""
72
+ return (self.path / "meta.json").stat().st_mtime
73
+
74
+ def pieces(self) -> list[Piece]:
75
+ """The pieces that the entry scanned, in scan order."""
76
+ return [Piece(**p) for p in json.loads((self.path / "pieces.json").read_text())]
77
+
78
+ def rows(self, name: str) -> np.ndarray:
79
+ """A per-row array: ``ids``, ``positions``, ``piece`` or ``counted``."""
80
+ return np.load(self.path / f"{name}.npy", mmap_mode="r")
81
+
82
+ def acts(self) -> np.memmap:
83
+ """The whole layer's activations, ``[rows, width]``, memory-mapped."""
84
+ shape = (self.meta["rows"], self.meta["width"])
85
+ return np.memmap(self.path / "acts.bin", dtype=self.meta["storage"], mode="r", shape=shape)
86
+
87
+ def column(self, neuron_idx: int) -> torch.Tensor:
88
+ """One neuron's activation on every row, as float32 (as the scan reads it)."""
89
+ return self._tensor(np.ascontiguousarray(self.acts()[:, neuron_idx])).float()
90
+
91
+ def _tensor(self, array: np.ndarray) -> torch.Tensor:
92
+ tensor = torch.from_numpy(np.array(array))
93
+ dtype = getattr(torch, self.meta["dtype"])
94
+ return tensor.view(dtype) if dtype in _BITS else tensor
95
+
96
+ def chunks(self) -> Iterator[Chunk]:
97
+ """The scan's batches again, as the model produced them."""
98
+ pieces = self.pieces()
99
+ acts = self.acts()
100
+ ids, positions = self.rows("ids"), self.rows("positions")
101
+ counted, piece = self.rows("counted"), self.rows("piece")
102
+ lengths = np.bincount(piece, minlength=len(pieces)).tolist()
103
+ start = row = 0
104
+ for size in self.meta["batches"]:
105
+ batch_lengths = lengths[start : start + size]
106
+ end = row + sum(batch_lengths)
107
+ yield Chunk(
108
+ pieces=pieces[start : start + size],
109
+ lengths=batch_lengths,
110
+ ids=torch.from_numpy(np.array(ids[row:end])).long(),
111
+ positions=torch.from_numpy(np.array(positions[row:end])).long(),
112
+ counted=torch.from_numpy(np.array(counted[row:end])),
113
+ acts=self._tensor(acts[row:end]),
114
+ )
115
+ start, row = start + size, end
116
+
117
+
118
+ class LayerCache:
119
+ """The cache of whole-layer scans under ``root``, capped at ``max_bytes``."""
120
+
121
+ def __init__(self, root: Path, max_bytes: int | None = None) -> None:
122
+ self.root = Path(root)
123
+ if max_bytes is None:
124
+ max_bytes = int(float(os.environ.get(SIZE_ENV, DEFAULT_GB)) * 1e9)
125
+ self.max_bytes = max_bytes
126
+ self.last: Path | None = None # the entry the last scan replayed or stored
127
+ self.replayed = False
128
+
129
+ @classmethod
130
+ def default(cls) -> LayerCache | None:
131
+ """The cache at the default location, or ``None`` when caching is off."""
132
+ root = default_root()
133
+ return None if root is None else cls(root)
134
+
135
+ # -- keys ----------------------------------------------------------------
136
+
137
+ @staticmethod
138
+ def fields(
139
+ runner: NeuronHookRunner, layer: int, pieces: Sequence[Piece], batch_tokens: int
140
+ ) -> dict | None:
141
+ """What an entry is keyed by; None for a model with no name to key it by."""
142
+ config = getattr(runner.model, "config", None)
143
+ model = getattr(runner, "model_name", None) or getattr(config, "_name_or_path", None)
144
+ if not model:
145
+ return None
146
+ try:
147
+ import transformers
148
+
149
+ transformers_version = transformers.__version__
150
+ except ImportError:
151
+ transformers_version = None
152
+ digest = hashlib.sha256()
153
+ for piece in pieces:
154
+ digest.update(json.dumps(asdict(piece), ensure_ascii=False).encode() + b"\n")
155
+ return {
156
+ "format": FORMAT,
157
+ "model": model,
158
+ "revision": getattr(runner, "model_revision", None),
159
+ "dtype": str(next(runner.model.parameters()).dtype).removeprefix("torch."),
160
+ "device": scan_device(
161
+ runner, pieces, layer, batch_tokens
162
+ ), # the ANE's activations differ from torch's
163
+ "torch": torch.__version__,
164
+ "transformers": transformers_version,
165
+ "layer": layer,
166
+ "batch_tokens": batch_tokens,
167
+ "pieces_sha256": digest.hexdigest(),
168
+ }
169
+
170
+ def path(self, fields: dict) -> Path:
171
+ """The entry directory for ``fields``."""
172
+ key = hashlib.sha256(json.dumps(fields, sort_keys=True).encode()).hexdigest()[:20]
173
+ return self.root / fields["model"].replace("/", "--") / f"L{fields['layer']}" / key
174
+
175
+ # -- reading -------------------------------------------------------------
176
+
177
+ def lookup(
178
+ self, runner: NeuronHookRunner, layer: int, pieces: Sequence[Piece], batch_tokens: int
179
+ ) -> Entry | None:
180
+ """The entry for this scan, or ``None`` when there is none."""
181
+ fields = self.fields(runner, layer, pieces, batch_tokens)
182
+ if fields is None:
183
+ return None
184
+ path = self.path(fields)
185
+ try:
186
+ meta = json.loads((path / "meta.json").read_text())
187
+ except (OSError, ValueError):
188
+ return None
189
+ os.utime(path / "meta.json") # least recently used goes first
190
+ return Entry(path, meta)
191
+
192
+ def entries(self) -> list[Entry]:
193
+ """Every complete entry in the cache."""
194
+ found = []
195
+ for meta in self.root.glob("*/L*/*/meta.json"):
196
+ if meta.parent.name.startswith("."):
197
+ continue # still being written
198
+ try:
199
+ found.append(Entry(meta.parent, json.loads(meta.read_text())))
200
+ except (OSError, ValueError):
201
+ continue
202
+ return sorted(found, key=lambda e: e.last_used)
203
+
204
+ def cached_layers(self, model: str) -> set[int]:
205
+ """The layers of ``model`` that the cache holds."""
206
+ entries = self.entries()
207
+ return {e.meta["fields"]["layer"] for e in entries if e.meta["fields"]["model"] == model}
208
+
209
+ # -- writing -------------------------------------------------------------
210
+
211
+ def through(
212
+ self,
213
+ runner: NeuronHookRunner,
214
+ layer: int,
215
+ pieces: Sequence[Piece],
216
+ batch_tokens: int,
217
+ chunks: Iterator[Chunk],
218
+ ) -> Iterator[Chunk]:
219
+ """The cached chunks if this scan is cached, else ``chunks``, stored as they pass."""
220
+ entry = self.lookup(runner, layer, pieces, batch_tokens)
221
+ self.replayed, self.last = entry is not None, entry and entry.path
222
+ if entry is not None:
223
+ return entry.chunks()
224
+ fields = self.fields(runner, layer, pieces, batch_tokens)
225
+ if fields is None:
226
+ return chunks
227
+ self.last = self.path(fields)
228
+ return self._record(fields, pieces, chunks)
229
+
230
+ def _record(self, fields: dict, pieces: Sequence[Piece], chunks: Iterator[Chunk]):
231
+ final = self.path(fields)
232
+ tmp = final.with_name(f".{final.name}.{os.getpid()}.{secrets.token_hex(4)}")
233
+ tmp.mkdir(parents=True)
234
+ rows: dict[str, list[np.ndarray]] = {"ids": [], "positions": [], "piece": [], "counted": []}
235
+ sizes: list[int] = []
236
+ index = 0
237
+ width = storage = None
238
+ try:
239
+ with open(tmp / "acts.bin", "wb") as fh:
240
+ for chunk in chunks:
241
+ acts = chunk.acts.contiguous()
242
+ bits = acts.view(_BITS[acts.dtype]) if acts.dtype in _BITS else acts
243
+ fh.write(bits.numpy().tobytes())
244
+ width, storage = acts.shape[1], str(bits.numpy().dtype)
245
+ rows["ids"].append(chunk.ids.numpy().astype(np.int32))
246
+ rows["positions"].append(chunk.positions.numpy().astype(np.int32))
247
+ rows["counted"].append(chunk.counted.numpy())
248
+ sizes.append(len(chunk.pieces))
249
+ for n in chunk.lengths:
250
+ rows["piece"].append(np.full(n, index, dtype=np.int32))
251
+ index += 1
252
+ yield chunk
253
+ _write_index(tmp, fields, pieces, rows, width, storage, sizes)
254
+ with suppress(OSError): # another scan stored it first
255
+ tmp.rename(final)
256
+ self.evict(keep=final)
257
+ finally:
258
+ shutil.rmtree(tmp, ignore_errors=True)
259
+
260
+ def evict(self, keep: Path | None = None) -> list[Entry]:
261
+ """Delete least recently used entries until the cache fits its cap."""
262
+ entries = self.entries()
263
+ total = sum(e.size for e in entries)
264
+ removed = []
265
+ for entry in entries:
266
+ if total <= self.max_bytes:
267
+ break
268
+ if entry.path == keep:
269
+ continue
270
+ total -= entry.size
271
+ shutil.rmtree(entry.path, ignore_errors=True)
272
+ removed.append(entry)
273
+ return removed
274
+
275
+ def clear(self) -> int:
276
+ """Delete every entry; returns how many there were."""
277
+ entries = self.entries()
278
+ for entry in entries:
279
+ shutil.rmtree(entry.path, ignore_errors=True)
280
+ return len(entries)
281
+
282
+
283
+ def _write_index(tmp: Path, fields: dict, pieces, rows: dict, width, storage, sizes) -> None:
284
+ """Write everything that an entry holds besides ``acts.bin``.
285
+
286
+ That is the per-row arrays, the pieces, and ``meta.json``.
287
+ """
288
+ for name, parts in rows.items():
289
+ np.save(tmp / f"{name}.npy", np.concatenate(parts) if parts else np.zeros(0))
290
+ (tmp / "pieces.json").write_text(json.dumps([asdict(p) for p in pieces]))
291
+ total = int(sum(len(p) for p in rows["ids"]))
292
+ meta = {
293
+ "fields": fields,
294
+ "rows": total,
295
+ "width": width or 0,
296
+ "dtype": fields["dtype"],
297
+ "storage": storage or "float32",
298
+ "batches": sizes,
299
+ "created": datetime.now(UTC).isoformat(timespec="seconds"),
300
+ }
301
+ (tmp / "meta.json").write_text(json.dumps(meta, indent=2))
@@ -0,0 +1,274 @@
1
+ """Run independent forward passes layer by layer over a streamed model.
2
+
3
+ A streamed model (``--memory stream``, :mod:`openinterp.harness.memory`) reads each
4
+ decoder layer from disk on every pass. The review makes hundreds of passes, one after
5
+ another, so it reads every layer hundreds of times. :func:`map_layer_major` runs the
6
+ items of a loop at once, each in its own thread. It runs each layer once for all the
7
+ passes that wait for it:
8
+
9
+ 1. Each thread runs the model's own forward, so each pass computes what it computes
10
+ alone. Only the order of the passes changes.
11
+ 2. Every thread waits at a layer or has finished. Then the scheduler loads the next
12
+ layer that a thread waits at, in the order of the layers, and lets those threads run
13
+ it. It then frees the layer and loads the next one. So the layers are read in sweeps
14
+ from the first to the last, and a sweep serves every waiting pass.
15
+ 3. While the threads run, a hook is applied only in the thread that registered it.
16
+ The harness's hooks stay where they are, on modules that every thread shares.
17
+
18
+ A model that isn't streamed runs the loop as before, one item after another.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import threading
24
+ from collections.abc import Callable, Iterator, Sequence
25
+ from contextlib import contextmanager
26
+ from typing import Any, TypeVar
27
+
28
+ import torch
29
+ from torch import nn
30
+
31
+ from openinterp.harness.memory import is_streamed, shared_by, stream_rows
32
+
33
+ T = TypeVar("T")
34
+ R = TypeVar("R")
35
+
36
+ # The most passes that run at once. Each holds its own activations.
37
+ MAX_WORKERS = 32
38
+
39
+ # The prompt length that :func:`workers_for` plans each pass's memory with.
40
+ PLANNED_TOKENS = 256
41
+
42
+ _active = False # a layer-major loop is running: a loop inside it runs in order
43
+
44
+
45
+ class LayerScheduler:
46
+ """Lets threads through the streamed layers in sweeps, one loaded layer at a time."""
47
+
48
+ def __init__(self, layers: Sequence[nn.Module]) -> None:
49
+ self.layers = list(layers)
50
+ self.streamed = [
51
+ [m for m in layer.modules() if getattr(getattr(m, "_hf_hook", None), "offload", False)]
52
+ for layer in self.layers
53
+ ]
54
+ self.cond = threading.Condition()
55
+ self.running = 0 # threads outside the scheduler's waits
56
+ self.inside = 0 # threads inside the loaded layer
57
+ self.waiting: dict[int, int] = {} # thread id -> the layer it waits at
58
+ self.loaded: int | None = None
59
+ self.loads = 0 # layers read from disk, for tests and reports
60
+
61
+ def start(self) -> None:
62
+ """A worker thread starts."""
63
+ with self.cond:
64
+ self.running += 1
65
+
66
+ def finish(self) -> None:
67
+ """A worker thread ends, normally or with an exception."""
68
+ with self.cond:
69
+ self.running -= 1
70
+ self._advance_if_idle()
71
+ self.cond.notify_all()
72
+
73
+ def enter(self, layer: int) -> None:
74
+ """Block until ``layer`` is loaded for this thread's pass."""
75
+ me = threading.get_ident()
76
+ with self.cond:
77
+ self.running -= 1
78
+ self.waiting[me] = layer
79
+ self._advance_if_idle()
80
+ self.cond.notify_all()
81
+ while self.loaded != layer or me not in self.waiting:
82
+ self.cond.wait()
83
+ del self.waiting[me]
84
+ self.running += 1
85
+ self.inside += 1
86
+
87
+ def leave(self, layer: int) -> None:
88
+ """This thread's pass has run ``layer``, or stopped in it."""
89
+ with self.cond:
90
+ self.inside -= 1
91
+
92
+ def _advance_if_idle(self) -> None:
93
+ # Called with the lock held. Only when no thread can still reach the loaded layer.
94
+ if self.running or self.inside or not self.waiting:
95
+ return
96
+ targets = set(self.waiting.values())
97
+ if self.loaded in targets:
98
+ return # its waiters are being let through
99
+ after = [layer for layer in targets if self.loaded is None or layer > self.loaded]
100
+ self._load(min(after) if after else min(targets))
101
+
102
+ def _load(self, layer: int) -> None:
103
+ if self.loaded is not None:
104
+ self._release(self.loaded)
105
+ for module in self.streamed[layer]:
106
+ module._hf_hook.pre_forward(module) # reads the weights, as each pass did
107
+ module._hf_hook.offload = False # so a pass doesn't read or free them again
108
+ self.loaded = layer
109
+ self.loads += 1
110
+
111
+ def _release(self, layer: int) -> None:
112
+ for module in self.streamed[layer]:
113
+ module._hf_hook.offload = True
114
+ module._hf_hook.post_forward(module, None) # frees the weights
115
+
116
+ def close(self) -> None:
117
+ """Free the layer that is still loaded."""
118
+ if self.loaded is not None:
119
+ self._release(self.loaded)
120
+ self.loaded = None
121
+
122
+ @contextmanager
123
+ def installed(self) -> Iterator[None]:
124
+ """Route every pass through :meth:`enter` and :meth:`leave` at each layer."""
125
+ originals = [layer.forward for layer in self.layers]
126
+
127
+ def through(index: int, forward: Callable) -> Callable:
128
+ def run(*args, **kwargs):
129
+ self.enter(index)
130
+ try:
131
+ return forward(*args, **kwargs)
132
+ finally:
133
+ self.leave(index)
134
+
135
+ return run
136
+
137
+ for index, (layer, forward) in enumerate(zip(self.layers, originals, strict=True)):
138
+ layer.forward = through(index, forward)
139
+ try:
140
+ with _hooks_per_thread():
141
+ yield
142
+ finally:
143
+ for layer, forward in zip(self.layers, originals, strict=True):
144
+ layer.forward = forward
145
+ self.close()
146
+
147
+
148
+ @contextmanager
149
+ def _hooks_per_thread() -> Iterator[None]:
150
+ """While it is open, a forward hook registered in a thread is applied only in it."""
151
+ register_hook, register_pre_hook = (
152
+ nn.Module.register_forward_hook,
153
+ nn.Module.register_forward_pre_hook,
154
+ )
155
+
156
+ def owned(fn: Callable) -> Callable:
157
+ owner = threading.get_ident()
158
+
159
+ def hook(*args, **kwargs):
160
+ return fn(*args, **kwargs) if threading.get_ident() == owner else None
161
+
162
+ return hook
163
+
164
+ def forward_hook(self, hook, *args, **kwargs):
165
+ return register_hook(self, owned(hook), *args, **kwargs)
166
+
167
+ def forward_pre_hook(self, hook, *args, **kwargs):
168
+ return register_pre_hook(self, owned(hook), *args, **kwargs)
169
+
170
+ nn.Module.register_forward_hook = forward_hook
171
+ nn.Module.register_forward_pre_hook = forward_pre_hook
172
+ try:
173
+ yield
174
+ finally:
175
+ nn.Module.register_forward_hook = register_hook
176
+ nn.Module.register_forward_pre_hook = register_pre_hook
177
+
178
+
179
+ @contextmanager
180
+ def _locked(obj: Any, name: str) -> Iterator[None]:
181
+ """Let one thread at a time call ``obj.name``: a fast tokenizer isn't thread-safe."""
182
+ own = vars(obj).get(name) # an attribute set on the instance, to put back after
183
+ method, lock = getattr(obj, name), threading.Lock()
184
+
185
+ def call(*args, **kwargs):
186
+ with lock:
187
+ return method(*args, **kwargs)
188
+
189
+ setattr(obj, name, call)
190
+ try:
191
+ yield
192
+ finally:
193
+ if own is None:
194
+ delattr(obj, name)
195
+ else:
196
+ setattr(obj, name, own)
197
+
198
+
199
+ def workers_for(runner, n_items: int, tokens: int, logits: bool) -> int:
200
+ """How many passes run at once: as many as a quarter of the planned memory holds.
201
+
202
+ Each pass is planned at ``tokens`` tokens, with its logits if ``logits``.
203
+ """
204
+ rows = stream_rows(runner, tokens, 1, logits=logits)
205
+ return max(1, min(n_items, MAX_WORKERS, rows))
206
+
207
+
208
+ def prompt_tokens(runner, texts: Sequence[str]) -> int:
209
+ """The token count of the longest of ``texts``: what a pass over one of them holds."""
210
+ return max((runner.encode([t])["input_ids"].shape[1] for t in texts), default=1)
211
+
212
+
213
+ def map_layer_major(
214
+ runner,
215
+ fn: Callable[[T], R],
216
+ items: Sequence[T],
217
+ *,
218
+ tokens: int = PLANNED_TOKENS,
219
+ logits: bool = True,
220
+ ) -> list[R]:
221
+ """``[fn(item) for item in items]``, run layer-major when ``runner``'s model streams.
222
+
223
+ ``tokens`` and ``logits`` describe the largest pass an item makes, so that the
224
+ passes that run at once fit in the planned memory (:func:`workers_for`). ``fn`` must
225
+ not change state that another item reads. The results are in the order of
226
+ ``items``. The first exception raised by an item is raised again.
227
+ """
228
+ global _active
229
+ items = list(items)
230
+ if len(items) < 2 or _active or not is_streamed(runner.model):
231
+ return [fn(item) for item in items] # a loop inside a running one runs in order
232
+ n_workers = workers_for(runner, len(items), tokens, logits)
233
+ scheduler = LayerScheduler(runner.layers)
234
+ _active = True
235
+ try:
236
+ with scheduler.installed(), _locked(runner, "encode"), shared_by(n_workers):
237
+ results = _run_workers(scheduler, fn, items, n_workers)
238
+ finally:
239
+ _active = False
240
+ runner.layer_loads = getattr(runner, "layer_loads", 0) + scheduler.loads
241
+ return results
242
+
243
+
244
+ def _run_workers(scheduler: LayerScheduler, fn: Callable, items: list, n_workers: int) -> list:
245
+ """Run ``fn`` over ``items`` in ``n_workers`` threads; raise the first exception again."""
246
+ results: list[Any] = [None] * len(items)
247
+ errors: list[BaseException] = []
248
+ queue = iter(range(len(items)))
249
+ lock = threading.Lock()
250
+ grad = torch.is_grad_enabled() # grad mode is per thread: carry the caller's over
251
+
252
+ def work() -> None:
253
+ torch.set_grad_enabled(grad)
254
+ try:
255
+ while not errors:
256
+ with lock:
257
+ index = next(queue, None)
258
+ if index is None:
259
+ return
260
+ results[index] = fn(items[index])
261
+ except BaseException as exc: # raised again in the calling thread
262
+ errors.append(exc)
263
+ finally:
264
+ scheduler.finish()
265
+
266
+ threads = [threading.Thread(target=work) for _ in range(n_workers)]
267
+ for thread in threads:
268
+ scheduler.start()
269
+ thread.start()
270
+ for thread in threads:
271
+ thread.join()
272
+ if errors:
273
+ raise errors[0]
274
+ return results
@@ -0,0 +1,93 @@
1
+ """Distinguish retrospective neurons from lookahead (next-token preparing) neurons.
2
+
3
+ Two complementary measurements on native weights:
4
+
5
+ * :func:`output_token_projection` reads the neuron's output direction through the
6
+ unembedding, to list the tokens that the pole directly promotes. This is a logit-lens
7
+ approximation that skips the final norm.
8
+ * :func:`measure_lookahead` zero-ablates the pole on real prompts and compares the
9
+ next-token distributions. A lookahead neuron's ablation moves probability mass
10
+ in a concentrated way onto/off a small set of upcoming tokens. :func:`ablation_profile`
11
+ does the same for any unit's ablation (an attention head is mean-ablated).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Mapping
17
+
18
+ import torch
19
+ import torch.nn.functional as F
20
+
21
+ from openinterp.core.schemas import LookaheadProfile, Pole
22
+ from openinterp.harness.hooks import Inputs, Intervention, NeuronHookRunner, ablate_pole
23
+
24
+
25
+ def output_token_projection(
26
+ runner: NeuronHookRunner, layer: int, neuron_idx: int, pole: Pole, k: int = 10
27
+ ) -> list[tuple[int, float]]:
28
+ """Top-``k`` ``(token_id, score)`` pairs promoted by the pole through the direct path."""
29
+ unembed = runner.model.get_output_embeddings().weight.detach() # [vocab, d_model]
30
+ direction = runner.neuron_output_direction(layer, neuron_idx).to(unembed.dtype)
31
+ scores = unembed @ direction
32
+ if pole == "negative":
33
+ scores = -scores
34
+ top = scores.topk(min(k, scores.numel()))
35
+ return list(zip(top.indices.tolist(), top.values.tolist(), strict=True))
36
+
37
+
38
+ def measure_lookahead(
39
+ runner: NeuronHookRunner,
40
+ prompts: Inputs,
41
+ layer: int,
42
+ neuron_idx: int,
43
+ pole: Pole,
44
+ **kwargs,
45
+ ) -> LookaheadProfile:
46
+ """Zero-ablate the pole on ``prompts``; see :func:`ablation_profile`."""
47
+ return ablation_profile(
48
+ runner, prompts, {"interventions": {layer: ablate_pole(neuron_idx, pole)}}, **kwargs
49
+ )
50
+
51
+
52
+ @torch.no_grad()
53
+ def ablation_profile(
54
+ runner: NeuronHookRunner,
55
+ prompts: Inputs,
56
+ ablation: Mapping[str, Mapping[int, Intervention]],
57
+ *,
58
+ k: int = 10,
59
+ kl_threshold: float = 1e-3,
60
+ concentration_threshold: float = 0.5,
61
+ ) -> LookaheadProfile:
62
+ """Ablate a unit on ``prompts`` and profile the effect on next-token predictions.
63
+
64
+ ``ablation`` holds :meth:`NeuronHookRunner.run_with_interventions` keyword arguments
65
+ (a unit's ``ablation()``). ``kl_divergence`` is KL(clean || ablated) averaged over
66
+ non-padding positions. The unit is flagged as lookahead when two things are true.
67
+ That KL exceeds ``kl_threshold``. And the ``k`` tokens that lose the most probability
68
+ hold at least ``concentration_threshold`` of the total probability mass moved.
69
+ """
70
+ encoded = runner.encode(prompts)
71
+ mask = encoded.get("attention_mask", torch.ones_like(encoded["input_ids"])).bool()
72
+ clean = F.log_softmax(runner.run_with_interventions(encoded).float(), dim=-1)[mask]
73
+ ablated = F.log_softmax(runner.run_with_interventions(encoded, **ablation).float(), dim=-1)[
74
+ mask
75
+ ]
76
+
77
+ kl = F.kl_div(ablated, clean, log_target=True, reduction="batchmean").clamp(min=0.0)
78
+ prob_shift = (clean.exp() - ablated.exp()).mean(dim=0) # positive = unit supports token
79
+ total_moved = prob_shift.abs().sum()
80
+ top = prob_shift.topk(min(k, prob_shift.numel()))
81
+ concentration = float(top.values.clamp(min=0).sum() / total_moved) if total_moved > 0 else 0.0
82
+
83
+ tokenizer = runner.tokenizer
84
+ tokens = [
85
+ tokenizer.decode([i]) if tokenizer is not None else str(i)
86
+ for i, v in zip(top.indices.tolist(), top.values.tolist(), strict=True)
87
+ if v > 0
88
+ ]
89
+ return LookaheadProfile(
90
+ is_lookahead=float(kl) >= kl_threshold and concentration >= concentration_threshold,
91
+ target_upcoming_tokens=tokens,
92
+ kl_divergence=float(kl),
93
+ )