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,200 @@
1
+ """Measure the multi-dimensional feature check (:class:`~openinterp.core.manifold.ManifoldCheck`).
2
+
3
+ The axes are the audited pole and its top partner poles. Each axis is divided by its
4
+ peak over every token of the top examples. So a partner with large activations doesn't
5
+ fill the first component. Unit variance would be worse: it would blow a partner that is
6
+ silent on a family, with tiny jitter, up into a full axis.
7
+
8
+ * **The activating set.** Each top example is run again, and the poles are read at the
9
+ audited pole's peak.
10
+ * **The families.** Each word goes into every carrier sentence of
11
+ :data:`openinterp.harness.specificity.CARRIERS`. A pole's strength on the word is its
12
+ peak over the word's tokens, as a mean over the carriers.
13
+
14
+ A circle's score follows Engels et al. (2024). Their first component is often an
15
+ intensity direction and the circle lies in components 2 and 3. So the score takes the
16
+ better of the planes 1–2 and 2–3. The p-value repeats that choice on each shuffled
17
+ order, so it pays for the choice.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import cmath
23
+ import math
24
+ from collections.abc import Sequence
25
+
26
+ import torch
27
+
28
+ from openinterp.core.manifold import FamilyManifold, ManifoldCheck, ManifoldPole
29
+ from openinterp.core.schemas import ACTIVATION_THRESHOLD, ActivatingExample, CorrelatedNeuron
30
+ from openinterp.core.stats import spearman
31
+ from openinterp.harness.hooks import NeuronHookRunner, pole_strength
32
+ from openinterp.harness.specificity import CARRIERS
33
+
34
+ PARTNERS = 5 # partner poles beside the audited pole
35
+ SHOWN = 3 # principal components whose explained variance is recorded
36
+ SHUFFLES = 999 # shuffled orders for each p-value
37
+ FAMILIES: dict[str, tuple[str, tuple[str, ...]]] = {
38
+ "weekdays": ("circle", ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
39
+ "Saturday", "Sunday")),
40
+ "months": ("circle", ("January", "February", "March", "April", "May", "June", "July",
41
+ "August", "September", "October", "November", "December")),
42
+ "numbers": ("line", ("one", "two", "three", "four", "five", "six", "seven", "eight",
43
+ "nine", "ten", "eleven", "twelve")),
44
+ } # fmt: skip
45
+
46
+
47
+ def axes(neuron_idx: int, pole, partners: Sequence[CorrelatedNeuron]) -> list[ManifoldPole]:
48
+ """The audited pole, then its top :data:`PARTNERS` partner poles."""
49
+ own = ManifoldPole(neuron_idx=neuron_idx, pole=pole)
50
+ return [own, *(ManifoldPole(neuron_idx=c.neuron_idx, pole=c.pole) for c in partners)][
51
+ : 1 + PARTNERS
52
+ ]
53
+
54
+
55
+ def _strengths(acts: torch.Tensor, poles: Sequence[ManifoldPole]) -> torch.Tensor:
56
+ """``[seq, len(poles)]``: each pole's strength at each token, from layer ``acts``."""
57
+ columns = [pole_strength(acts[:, p.neuron_idx], p.pole) for p in poles]
58
+ return torch.stack(columns, dim=1).float().cpu()
59
+
60
+
61
+ def _layer(runner: NeuronHookRunner, text: str, layer: int) -> torch.Tensor:
62
+ """``[seq, width]``: the MLP hidden activations of ``layer`` on ``text``."""
63
+ return runner.layer_activations(text, [layer])[layer][0]
64
+
65
+
66
+ def word_vector(runner, layer: int, poles, word: str) -> torch.Tensor | None:
67
+ """Each pole's peak over ``word``'s tokens, as a mean over the carriers.
68
+
69
+ ``None`` when no carrier measures a token of the word.
70
+ """
71
+ found = []
72
+ for carrier in CARRIERS:
73
+ text, start = carrier.format(word), carrier.index("{}")
74
+ offsets = runner.tokenizer(text, return_offsets_mapping=True)["offset_mapping"]
75
+ values = _strengths(_layer(runner, text, layer), poles)
76
+ held = [i for i, (a, b) in enumerate(offsets) if a < start + len(word) and b > start]
77
+ if len(offsets) == len(values) and held:
78
+ found.append(values[held].max(dim=0).values)
79
+ return torch.stack(found).mean(dim=0) if found else None
80
+
81
+
82
+ def peak_vector(runner, layer: int, poles, example: ActivatingExample, skip_first: bool):
83
+ """The poles at the audited pole's peak, and each pole's peak over the example.
84
+
85
+ The example's text is run again.
86
+ """
87
+ values = _strengths(_layer(runner, example.text, layer), poles)
88
+ start = int(skip_first and len(values) > 1)
89
+ return values[start + int(values[start:, 0].argmax())], values[start:].max(dim=0).values
90
+
91
+
92
+ def scaled(rows: torch.Tensor, scales: torch.Tensor) -> torch.Tensor:
93
+ """``rows`` with each pole divided by its scale; a pole with no scale is 0."""
94
+ return torch.where(scales > 0, rows / scales.clamp_min(1e-12), torch.zeros_like(rows))
95
+
96
+
97
+ def pca(rows: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
98
+ """The coordinates, the variances and the unit components of the centred ``rows``."""
99
+ z = rows.double() - rows.double().mean(dim=0)
100
+ u, s, vt = torch.linalg.svd(z, full_matrices=False)
101
+ return u * s, s**2, vt
102
+
103
+
104
+ def ratios(variances: torch.Tensor) -> list[float]:
105
+ """The first :data:`SHOWN` shares of variance, or none when nothing varies."""
106
+ total = float(variances.sum())
107
+ return [round(float(v) / total, 4) for v in variances[:SHOWN]] if total > 0 else []
108
+
109
+
110
+ def participation_ratio(variances: torch.Tensor) -> float:
111
+ """``(Σλ)² / Σλ²``: the effective number of dimensions; 0 when nothing varies."""
112
+ squares = float((variances**2).sum())
113
+ return round(float(variances.sum()) ** 2 / squares, 4) if squares > 0 else 0.0
114
+
115
+
116
+ def circle_score(coords: torch.Tensor, places: Sequence[int], n: int) -> float:
117
+ """The mean resultant length of angle minus cycle place, in the better direction."""
118
+ angles = [math.atan2(float(y), float(x)) for x, y in coords]
119
+ cycle = [2 * math.pi * k / n for k in places]
120
+ forward = abs(sum(cmath.exp(1j * (a - c)) for a, c in zip(angles, cycle, strict=True)))
121
+ backward = abs(sum(cmath.exp(1j * (a + c)) for a, c in zip(angles, cycle, strict=True)))
122
+ return max(forward, backward) / len(angles)
123
+
124
+
125
+ def _best(shape: str, coords: torch.Tensor, order: Sequence[int]) -> tuple[float, list[int]]:
126
+ """The best score over the allowed components, and those components (1-based).
127
+
128
+ On a tie the first components win. With no components the score is 0.
129
+ """
130
+ if shape == "circle":
131
+ planes = [[0, 1], [1, 2]][: max(coords.shape[1] - 1, 0)]
132
+ found = [(circle_score(coords[:, p], order, len(order)), p) for p in planes]
133
+ else:
134
+ values = torch.tensor(order, dtype=torch.float64)
135
+ found = [
136
+ (abs(spearman(coords[:, k], values)), [k]) for k in range(min(SHOWN, coords.shape[1]))
137
+ ]
138
+ score, picked = max(found, key=lambda f: f[0], default=(0.0, []))
139
+ return score, [k + 1 for k in picked]
140
+
141
+
142
+ def family_manifold(
143
+ family: str, rows: torch.Tensor, threshold: float, scales: torch.Tensor | None = None
144
+ ) -> FamilyManifold:
145
+ """The PCA and shape of one family's ``rows`` (members in order, poles as columns).
146
+
147
+ ``threshold`` is in the audited pole's units. The PCA sees ``rows`` divided by
148
+ ``scales`` (:func:`scaled`), when given.
149
+ """
150
+ shape = FAMILIES[family][0]
151
+ coords, variances, vt = pca(rows if scales is None else scaled(rows, scales))
152
+ order = list(range(len(rows)))
153
+ if not float(variances.sum()) > 0: # nothing varies: no shape and no components
154
+ coords = coords[:, :0]
155
+ score, components = _best(shape, coords, order)
156
+ rng = torch.Generator().manual_seed(len(rows)) # fixed: a record always replays
157
+ beaten = sum(
158
+ _best(shape, coords, torch.randperm(len(rows), generator=rng).tolist())[0] >= score - 1e-9
159
+ for _ in range(SHUFFLES)
160
+ )
161
+ share = float((vt[[c - 1 for c in components], 0] ** 2).sum()) if components else 0.0
162
+ plane = sorted((float(variances[c - 1]) for c in components), reverse=True)
163
+ round_ = plane[1] / plane[0] if shape == "circle" and len(plane) == 2 and plane[0] > 0 else None
164
+ return FamilyManifold(
165
+ family=family, shape=shape, members=len(rows),
166
+ active_members=int((rows[:, 0] >= threshold).sum()),
167
+ explained_variance=ratios(variances), components=components,
168
+ score=round(min(score, 1.0), 4), p_value=round((1 + beaten) / (1 + SHUFFLES), 4),
169
+ pole_share=round(min(share, 1.0), 4),
170
+ roundness=None if round_ is None else round(round_, 4),
171
+ ) # fmt: skip
172
+
173
+
174
+ def manifold_check(
175
+ runner: NeuronHookRunner,
176
+ layer: int,
177
+ poles: list[ManifoldPole],
178
+ examples: Sequence[ActivatingExample],
179
+ skip_first: bool = False,
180
+ ) -> ManifoldCheck:
181
+ """Measure the check for ``poles[0]`` and its partners in ``layer``."""
182
+ max_act = max((e.max_activation for e in examples), default=0.0)
183
+ measured = [peak_vector(runner, layer, poles, e, skip_first) for e in examples]
184
+ peaks = [p for p, _ in measured]
185
+ scales = torch.stack([s for _, s in measured]).max(dim=0).values if measured else None
186
+ variances = pca(scaled(torch.stack(peaks), scales))[1] if len(peaks) > 1 else torch.zeros(0)
187
+ families = []
188
+ for family, (_, words) in FAMILIES.items():
189
+ vectors = [word_vector(runner, layer, poles, w) for w in words]
190
+ if all(v is not None for v in vectors):
191
+ rows = torch.stack(vectors)
192
+ threshold = ACTIVATION_THRESHOLD * max_act
193
+ families.append(family_manifold(family, rows, threshold, scales))
194
+ return ManifoldCheck(
195
+ poles=poles,
196
+ activating_examples=len(peaks),
197
+ activating_explained_variance=ratios(variances),
198
+ participation_ratio=participation_ratio(variances),
199
+ families=families,
200
+ )
@@ -0,0 +1,471 @@
1
+ """Streaming mode: run a model larger than this machine's memory, slowly.
2
+
3
+ In streaming mode the decoder layers stay in the checkpoint's safetensors files on disk.
4
+ ``accelerate`` reads a layer's weights when the forward pass reaches it, and frees them
5
+ after. The embeddings, the final norm and the output head stay in memory. Peak memory is
6
+ then about one layer, those modules and one batch's activations. The cost is time: every
7
+ forward pass reads its layers from disk again.
8
+
9
+ It is never the default for a model that fits. ``--memory auto`` (the default) streams
10
+ only when the weights at the chosen dtype, plus :data:`WORKING_MARGIN_BYTES`, exceed free
11
+ RAM plus free swap. ``--memory stream`` forces it, and ``--memory normal`` never streams.
12
+ The maths is the same, so a streamed record reproduces in normal mode.
13
+
14
+ A streamed scan reads its layers once per batch, so it uses larger batches
15
+ (:func:`stream_batch_tokens`) to read each layer fewer times.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from collections.abc import Iterator, Mapping
22
+ from contextlib import AbstractContextManager, contextmanager, nullcontext, suppress
23
+ from dataclasses import dataclass
24
+ from pathlib import Path
25
+ from typing import Literal
26
+
27
+ import click
28
+ import torch
29
+ from torch import nn
30
+
31
+ MemoryMode = Literal["auto", "normal", "stream"]
32
+ MEMORY_MODES: tuple[MemoryMode, ...] = ("auto", "normal", "stream")
33
+
34
+ # What a scan or review holds beyond the weights, measured on CPU in float32. It is about
35
+ # 2.5–3 GB for GPT-2 and Qwen2.5-0.5B and 1.5B (docs/experiments.md, Memory).
36
+ WORKING_MARGIN_BYTES = 3 * 2**30
37
+
38
+ # A streamed scan's batch: as many tokens as its activations fit in, within these bounds.
39
+ # The floor is the normal scan's batch (harness/scan.py, BATCH_TOKENS).
40
+ STREAM_BATCH_TOKENS_MAX = 65536
41
+
42
+ # The most rows a streamed pass of single prompts runs at once (:func:`stream_rows`).
43
+ STREAM_ROWS_MAX = 64
44
+
45
+ MEMORY_ENV = "OPENINTERP_MEMORY_GB" # the memory to plan for, in place of what is free
46
+
47
+ _requested: MemoryMode = "auto"
48
+
49
+
50
+ def request(mode: MemoryMode) -> None:
51
+ """Set the mode that the next model load uses (the ``--memory`` option)."""
52
+ global _requested
53
+ _requested = mode
54
+
55
+
56
+ def requested() -> MemoryMode:
57
+ """The mode that ``--memory`` asked for."""
58
+ return _requested
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class MemoryPlan:
63
+ """Whether to stream a model, and the numbers that decided it."""
64
+
65
+ stream: bool
66
+ need_bytes: int | None # weights at the chosen dtype, plus the working margin
67
+ available_bytes: int | None # free RAM plus free swap
68
+ reason: str
69
+
70
+
71
+ def available_bytes(device: str | torch.device | None = None) -> int | None:
72
+ """Free memory to plan for on ``device``, or ``None`` when it can't be read.
73
+
74
+ On a CUDA device that is the GPU's free memory. Elsewhere it is free RAM plus free
75
+ swap. ``device`` defaults to the one the last model loaded on. ``$OPENINTERP_MEMORY_GB``
76
+ overrides it, to run as if only that much were free: on a shared machine, or to
77
+ test streaming on a large one.
78
+ """
79
+ budget = os.environ.get(MEMORY_ENV)
80
+ if budget:
81
+ return int(float(budget) * 2**30)
82
+ device = torch.device(device if device is not None else _budget_device)
83
+ try:
84
+ if device.type == "cuda":
85
+ return int(torch.cuda.mem_get_info(device)[0])
86
+ import psutil
87
+
88
+ return int(psutil.virtual_memory().available + psutil.swap_memory().free)
89
+ except Exception: # psutil missing, an unsupported platform, or no CUDA
90
+ return None
91
+
92
+
93
+ _budget_device = "cpu" # where the last model was loaded: what available_bytes plans for
94
+
95
+
96
+ def checkpoint_bytes(snapshot: Path) -> int:
97
+ """Total size of the safetensors (else PyTorch) weight files in a model snapshot."""
98
+ files = list(snapshot.glob("*.safetensors")) or list(snapshot.glob("*.bin"))
99
+ return sum(f.stat().st_size for f in files)
100
+
101
+
102
+ def weights_bytes(snapshot: Path, config, dtype: torch.dtype | str) -> int:
103
+ """The weights' size in memory at ``dtype``: the checkpoint scaled from its own dtype."""
104
+ stored = getattr(config, "torch_dtype", None) or getattr(config, "dtype", None)
105
+ size = checkpoint_bytes(snapshot)
106
+ if dtype == "auto" or stored is None:
107
+ return size
108
+ stored = getattr(torch, stored) if isinstance(stored, str) else stored
109
+ return size * _itemsize(dtype) // _itemsize(stored)
110
+
111
+
112
+ def plan(
113
+ mode: MemoryMode, need: int | None, have: int | None, where: str = "RAM and swap"
114
+ ) -> MemoryPlan:
115
+ """Decide whether to stream: always for ``stream``, never for ``normal``.
116
+
117
+ For ``auto``, stream only when ``need`` is known to exceed ``have``, the free
118
+ memory ``where`` names.
119
+ """
120
+ if mode == "stream":
121
+ return MemoryPlan(True, need, have, "--memory stream")
122
+ if mode == "normal" or need is None or have is None:
123
+ return MemoryPlan(False, need, have, f"--memory {mode}")
124
+ if need <= have:
125
+ return MemoryPlan(False, need, have, "the model fits")
126
+ reason = f"it needs about {_gb(need)} and {_gb(have)} is free ({where})"
127
+ return MemoryPlan(True, need, have, reason)
128
+
129
+
130
+ def warn(memory: MemoryPlan, model_name: str) -> None:
131
+ """Say on stderr that ``model_name`` streams from disk, and why."""
132
+ click.echo(
133
+ f"Streaming {model_name} from disk, because {memory.reason}. This is much "
134
+ "slower: every forward pass reads its layers from disk. Pass --memory normal "
135
+ "to load it whole.",
136
+ err=True,
137
+ )
138
+
139
+
140
+ def load_causal_lm(model_name: str, device: str, dtype, revision: str | None, **extra):
141
+ """Load ``model_name`` whole on ``device``, or streamed from disk (see the module doc).
142
+
143
+ A streamed model runs on the CPU. Its offloaded layers read the checkpoint files in
144
+ place, so streaming writes no copy of them. A mixture-of-experts checkpoint that
145
+ stores each expert apart is the exception. transformers stacks its experts as it
146
+ loads, and streaming writes the stacks to :func:`offload_dir`.
147
+ """
148
+ from transformers import AutoModelForCausalLM
149
+
150
+ from openinterp.harness.moe import use_openinterp_experts
151
+
152
+ global _budget_device
153
+ memory = _plan_load(model_name, dtype, revision, device)
154
+ if not memory.stream:
155
+ kwargs = {"dtype": dtype, "revision": revision, **extra}
156
+ model = AutoModelForCausalLM.from_pretrained(model_name, **kwargs).to(device)
157
+ use_openinterp_experts(model)
158
+ _budget_device = device
159
+ return model
160
+ warn(memory, model_name)
161
+ resident = stream_device(device)
162
+ if resident != device:
163
+ click.echo(f"Streaming runs on the CPU, not {device}.", err=True)
164
+ empty = _empty_model(model_name, revision)
165
+ device_map = stream_device_map(empty, _layers_path(empty, model_name), resident)
166
+ model = AutoModelForCausalLM.from_pretrained(
167
+ model_name,
168
+ dtype=dtype,
169
+ revision=revision,
170
+ device_map=device_map,
171
+ offload_folder=str(offload_dir()),
172
+ **extra,
173
+ )
174
+ keep_checkpoint_open(model, resident)
175
+ use_openinterp_experts(model) # reads only the routed experts (harness/moe.py)
176
+ _budget_device = resident
177
+ return model
178
+
179
+
180
+ def stream_device(device: str) -> str:
181
+ """Where a streamed model runs: ``device`` if it is CUDA, else the CPU.
182
+
183
+ On CUDA the embeddings, the final norm and the head stay on the GPU. Each layer is
184
+ read from disk and copied to the GPU when a pass reaches it. ``from_pretrained``
185
+ with a device map on MPS ends in a segmentation fault (transformers 5.20), so MPS
186
+ streams on the CPU.
187
+ """
188
+ return device if torch.device(device).type == "cuda" else "cpu"
189
+
190
+
191
+ class OpenCheckpoint(Mapping):
192
+ """``accelerate``'s offloaded weights, read from safetensors files that stay open.
193
+
194
+ ``accelerate`` opens the checkpoint file again for each tensor it reads. On
195
+ Qwen2.5-0.5B that took about 19 ms a tensor, and a pass reads 288 tensors. So it was
196
+ 5.6 s of a 5.9 s pass over 16 tokens. With each file opened once, the pass took
197
+ 0.18 s, and 0.14 s whole. The tensors read are the same. An open file is
198
+ memory-mapped. The pages it reads are in the file cache, which the system can free.
199
+ They are not the process's own memory.
200
+ """
201
+
202
+ def __init__(self, loader, device: str = "cpu") -> None:
203
+ self.loader = loader # accelerate's OffloadedWeightsLoader
204
+ self.files: dict[str, object] = {}
205
+ self.device = torch.device(device) # where the layers run: tensors are sent there
206
+
207
+ def __getitem__(self, key: str) -> torch.Tensor:
208
+ info = self.loader.index.get(key)
209
+ if key in self.loader.state_dict or not info or not info.get("safetensors_file"):
210
+ return self.loader[key]
211
+ path = info["safetensors_file"]
212
+ if path not in self.files:
213
+ from safetensors import safe_open
214
+
215
+ self.files[path] = safe_open(path, framework="pt", device="cpu")
216
+ tensor = self.files[path].get_tensor(info.get("weight_name", key))
217
+ return self._sent(tensor, info)
218
+
219
+ def slice(self, key: str, index: int) -> torch.Tensor:
220
+ """Row ``index`` of the stacked tensor ``key``, read without reading the rest.
221
+
222
+ A mixture-of-experts layer stacks its experts in one tensor. A pass reads only
223
+ the experts its tokens are routed to (:mod:`openinterp.harness.moe`).
224
+ """
225
+ info = self.loader.index.get(key)
226
+ if key in self.loader.state_dict or not info or not info.get("safetensors_file"):
227
+ return self.loader[key][index]
228
+ path = info["safetensors_file"]
229
+ if path not in self.files:
230
+ from safetensors import safe_open
231
+
232
+ self.files[path] = safe_open(path, framework="pt", device="cpu")
233
+ row = self.files[path].get_slice(info.get("weight_name", key))[index]
234
+ return self._sent(row, info)
235
+
236
+ def _sent(self, tensor: torch.Tensor, info: dict) -> torch.Tensor:
237
+ """``tensor`` on the layers' device, then at its dtype.
238
+
239
+ On a GPU it is sent in the checkpoint's dtype and converted there. Take a float32
240
+ run of Qwen2.5-0.5B, whose checkpoint is bfloat16. A streamed pass then took 0.66 s
241
+ on an RTX 4070 Ti, against 2.8 s converted on the CPU first.
242
+ """
243
+ tensor = tensor.to(self.device)
244
+ return tensor.to(getattr(torch, info["dtype"])) if "dtype" in info else tensor
245
+
246
+ def __iter__(self) -> Iterator[str]:
247
+ return iter(self.loader)
248
+
249
+ def __len__(self) -> int:
250
+ return len(self.loader)
251
+
252
+
253
+ def keep_checkpoint_open(model: nn.Module, device: str = "cpu") -> None:
254
+ """Make every streamed module of ``model`` read through one :class:`OpenCheckpoint`.
255
+
256
+ ``device`` is where the layers run (:func:`stream_device`).
257
+ """
258
+ from accelerate.utils import PrefixedDataset
259
+
260
+ opened: dict[int, OpenCheckpoint] = {}
261
+ for module in model.modules():
262
+ weights = getattr(getattr(module, "_hf_hook", None), "weights_map", None)
263
+ if isinstance(weights, PrefixedDataset) and hasattr(weights.dataset, "index"):
264
+ key = id(weights.dataset)
265
+ weights.dataset = opened.setdefault(key, OpenCheckpoint(weights.dataset, device))
266
+
267
+
268
+ def offload_dir() -> Path:
269
+ """Where ``accelerate`` may write weights it can't read in place (a ``.bin`` checkpoint).
270
+
271
+ ``$XDG_CACHE_HOME/openinterp/offload``, else ``~/.cache/openinterp/offload``. It stays
272
+ empty for a safetensors checkpoint.
273
+ """
274
+ root = os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache"
275
+ return Path(root) / "openinterp" / "offload"
276
+
277
+
278
+ def _plan_load(model_name: str, dtype, revision: str | None, device: str = "cpu") -> MemoryPlan:
279
+ """The streaming decision for loading ``model_name`` at ``dtype`` on ``device``."""
280
+ mode = requested()
281
+ if mode == "normal":
282
+ return plan(mode, None, None)
283
+ need = None
284
+ # Offline, or an unknown layout: don't stream unless asked.
285
+ with suppress(Exception):
286
+ snapshot = _snapshot(model_name, revision)
287
+ from transformers import AutoConfig
288
+
289
+ config = AutoConfig.from_pretrained(model_name, revision=revision)
290
+ need = weights_bytes(snapshot, config, dtype) + WORKING_MARGIN_BYTES
291
+ on_gpu = torch.device(device).type == "cuda"
292
+ return plan(mode, need, available_bytes(device), "on the GPU" if on_gpu else "RAM and swap")
293
+
294
+
295
+ def _snapshot(model_name: str, revision: str | None) -> Path:
296
+ """The local folder that holds ``model_name``'s weights, downloading them if needed."""
297
+ if Path(model_name).is_dir():
298
+ return Path(model_name)
299
+ from huggingface_hub import snapshot_download
300
+
301
+ patterns = ["*.json", "*.safetensors"]
302
+ return Path(snapshot_download(model_name, revision=revision, allow_patterns=patterns))
303
+
304
+
305
+ def _empty_model(model_name: str, revision: str | None) -> nn.Module:
306
+ """``model_name``'s architecture with every weight on the meta device (no memory)."""
307
+ from accelerate import init_empty_weights
308
+ from transformers import AutoConfig, AutoModelForCausalLM
309
+
310
+ config = AutoConfig.from_pretrained(model_name, revision=revision)
311
+ with init_empty_weights():
312
+ return AutoModelForCausalLM.from_config(config)
313
+
314
+
315
+ def _layers_path(model: nn.Module, model_name: str) -> str:
316
+ """The dotted path of ``model``'s decoder layers."""
317
+ from openinterp.harness.hooks import LAYER_PATHS, _try_path
318
+
319
+ for path in LAYER_PATHS:
320
+ layers = _try_path(model, path)
321
+ if isinstance(layers, nn.ModuleList) and len(layers) > 0:
322
+ return path
323
+ from openinterp.core.exceptions import ArchitectureNotSupportedError
324
+
325
+ raise ArchitectureNotSupportedError(f"no decoder layers found on {model_name}")
326
+
327
+
328
+ def stream_device_map(model: nn.Module, layers_path: str, resident: str = "cpu") -> dict[str, str]:
329
+ """An ``accelerate`` device map with each decoder layer on disk, the rest on ``resident``.
330
+
331
+ ``model`` may be on the meta device. ``layers_path`` names its decoder layers.
332
+ ``accelerate`` runs the layers on the first device in the map that isn't the CPU or
333
+ the disk. So they run on the GPU when ``resident`` is one.
334
+ """
335
+ device_map: dict[str, str] = {}
336
+
337
+ def assign(module: nn.Module, prefix: str) -> None:
338
+ for name, child in module.named_children():
339
+ path = f"{prefix}{name}"
340
+ if path == layers_path:
341
+ device_map.update({f"{path}.{i}": "disk" for i in range(len(child))})
342
+ elif layers_path.startswith(path + "."):
343
+ assign(child, path + ".")
344
+ else:
345
+ device_map[path] = resident
346
+
347
+ assign(model, "")
348
+ return device_map
349
+
350
+
351
+ def stream_batch_tokens(config, dtype: torch.dtype, have: int | None, floor: int) -> int:
352
+ """A streamed scan's batch: the tokens whose activations fit in half of ``have``.
353
+
354
+ One token of one layer holds about 4 residual-width and 3 MLP-width vectors at once
355
+ (the residual, attention and MLP inputs and outputs). The result is between
356
+ ``floor`` and :data:`STREAM_BATCH_TOKENS_MAX`.
357
+ """
358
+ width = getattr(config, "hidden_size", None) or getattr(config, "n_embd", None)
359
+ inner = getattr(config, "intermediate_size", None) or getattr(config, "n_inner", None)
360
+ if not width or have is None:
361
+ return floor
362
+ per_token = _itemsize(dtype) * (4 * width + 3 * (inner or 4 * width))
363
+ tokens = (have // 2) // per_token
364
+ return int(max(floor, min(STREAM_BATCH_TOKENS_MAX, tokens)))
365
+
366
+
367
+ def stream_rows(runner, tokens_per_row: int, default: int, *, logits: bool) -> int:
368
+ """Rows per forward pass: ``default``, or more when ``runner``'s model streams.
369
+
370
+ Every streamed pass reads its layers from disk again. So a streamed model runs as
371
+ many rows at once as a quarter of the planned memory holds (:func:`available_bytes`).
372
+ A pass that returns ``logits`` holds them and one log-softmax copy: 1.2 MB a token at
373
+ Qwen2.5's 151,936 tokens. Rows are independent, so only float rounding changes. The
374
+ result is at least ``default`` and at most :data:`STREAM_ROWS_MAX`.
375
+ """
376
+ have = available_bytes()
377
+ if not is_streamed(runner.model) or have is None:
378
+ return default
379
+ have //= _sharing # passes that run at once share the memory (layer_major)
380
+ config = runner.model.config
381
+ dtype = next(runner.model.parameters()).dtype
382
+ width = getattr(config, "hidden_size", None) or getattr(config, "n_embd", 1)
383
+ inner = getattr(config, "intermediate_size", None) or getattr(config, "n_inner", None)
384
+ per_token = _itemsize(dtype) * (4 * width + 3 * (inner or 4 * width))
385
+ if logits:
386
+ per_token += 2 * 4 * getattr(config, "vocab_size", 0) # float32 logits and log-probs
387
+ rows = (have // 4) // max(1, per_token * max(1, tokens_per_row))
388
+ return int(max(default, min(STREAM_ROWS_MAX, rows)))
389
+
390
+
391
+ _sharing = 1 # how many passes run at once (openinterp.harness.layer_major)
392
+
393
+
394
+ @contextmanager
395
+ def shared_by(passes: int) -> Iterator[None]:
396
+ """Plan each pass's memory as a share of the budget while ``passes`` run at once."""
397
+ global _sharing
398
+ previous, _sharing = _sharing, max(1, passes)
399
+ try:
400
+ yield
401
+ finally:
402
+ _sharing = previous
403
+
404
+
405
+ def loaded(module: nn.Module) -> AbstractContextManager:
406
+ """A context in which ``module``'s own weights are real tensors, also when streamed.
407
+
408
+ A streamed module keeps its weights on the meta device between passes. Read them
409
+ inside this context, and copy what must outlive it. Child modules are not loaded.
410
+ """
411
+ if getattr(module, "_hf_hook", None) is None:
412
+ return nullcontext()
413
+ from accelerate.utils import align_module_device
414
+
415
+ return align_module_device(module)
416
+
417
+
418
+ @contextmanager
419
+ def recompute_below(runner, layer: int) -> Iterator[None]:
420
+ """In a streamed model, recompute the layers below ``layer`` in a backward pass.
421
+
422
+ Autograd keeps the weights a layer used until the backward pass. So a gradient
423
+ through streamed layers holds all of them at once. Inside this block each layer
424
+ below ``layer`` keeps only its input. The backward pass runs it again, which reads
425
+ its weights from disk again. The values are the same. Take Qwen2.5-1.5B at layer
426
+ 11, with frozen weights (``cli._load``). Integrated gradients over 1,024 tokens
427
+ peaked at 1.8 GB with this, and at 4.5 GB without. It took 5.2 s, not 4.2 s.
428
+
429
+ ``layer`` itself isn't recomputed, since a hook there may end the forward pass. The
430
+ key-value cache is off, since a recomputed layer would add to it twice. A model that
431
+ isn't streamed is left as it is.
432
+ """
433
+ if not is_streamed(runner.model):
434
+ yield
435
+ return
436
+ from torch.utils.checkpoint import checkpoint
437
+
438
+ def recomputed(forward):
439
+ def run(*args, **kwargs):
440
+ if not torch.is_grad_enabled():
441
+ return forward(*args, **kwargs)
442
+ return checkpoint(forward, *args, use_reentrant=False, **kwargs)
443
+
444
+ return run
445
+
446
+ config = runner.model.config
447
+ use_cache, config.use_cache = getattr(config, "use_cache", None), False
448
+ blocks = list(runner.layers)[:layer]
449
+ forwards = [block.forward for block in blocks]
450
+ for block, forward in zip(blocks, forwards, strict=True):
451
+ block.forward = recomputed(forward)
452
+ try:
453
+ yield
454
+ finally:
455
+ for block, forward in zip(blocks, forwards, strict=True):
456
+ block.forward = forward
457
+ config.use_cache = use_cache
458
+
459
+
460
+ def is_streamed(model: nn.Module) -> bool:
461
+ """Whether some of ``model``'s weights stay on disk between passes."""
462
+ return any(p.device.type == "meta" for p in model.parameters())
463
+
464
+
465
+ def _itemsize(dtype: torch.dtype | str) -> int:
466
+ dtype = getattr(torch, dtype) if isinstance(dtype, str) else dtype
467
+ return torch.empty((), dtype=dtype).element_size()
468
+
469
+
470
+ def _gb(n: int) -> str:
471
+ return f"{n / 2**30:.1f} GB"