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,89 @@
1
+ """Explore draws weighted by a pole's effect on the loss (issue #3486).
2
+
3
+ On GPT-2, VERIFIED poles bring back no more of a layer's loss than random poles do
4
+ (``docs/analyses/registry-coverage.md``). So the registry may favour poles that are easy
5
+ to explain over poles that matter. So a share of explore draws (``[tool.openinterp]
6
+ effect_share``) picks a pole in proportion to its estimated effect. The effects come from
7
+ the model's table in ``data/pole-effects/`` (``open-interpretability pole-effects``). These
8
+ draws record ``selection: attribution``, so they never count towards the registry
9
+ statistics, which use only random draws. A model with no table always draws uniformly.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import random
16
+ from pathlib import Path
17
+
18
+ EFFECTS_DIR = Path("data") / "pole-effects"
19
+ DEFAULT_EFFECT_SHARE = 0.3
20
+
21
+
22
+ def table_path(root: Path, model: str) -> Path:
23
+ """Where ``model``'s table of pole effects is kept, under the repository ``root``."""
24
+ return root / EFFECTS_DIR / f"{model.replace('/', '--')}.json"
25
+
26
+
27
+ def effect_share(pyproject: Path) -> float:
28
+ """``[tool.openinterp] effect_share``: the share of explore draws weighted by effect."""
29
+ from openinterp.planner import _config
30
+
31
+ return float(_config(pyproject).get("effect_share", DEFAULT_EFFECT_SHARE))
32
+
33
+
34
+ def unit_slug(model: str, row: dict) -> str:
35
+ """The slug of the pole that a table row names."""
36
+ from openinterp.core.identifiers import NeuronIdentifier
37
+
38
+ return NeuronIdentifier(
39
+ model_name=model, layer=row["layer"], neuron_idx=row["neuron"], pole=row["pole"]
40
+ ).slug
41
+
42
+
43
+ def weighted_pole(rng: random.Random, model: str, rows: list[dict], taken: set[str]):
44
+ """A row drawn in proportion to its effect, among untaken poles that raise the loss.
45
+
46
+ ``taken`` holds studied slugs and the claim keys of open PRs.
47
+ """
48
+ from openinterp.planner import claim_key
49
+
50
+ slugs = {r_id: unit_slug(model, r) for r_id, r in enumerate(rows)}
51
+ free = [
52
+ r
53
+ for r_id, r in enumerate(rows)
54
+ if r["effect"] > 0 and not {slugs[r_id], claim_key(slugs[r_id])} & taken
55
+ ]
56
+ if not free:
57
+ return None
58
+ return rng.choices(free, weights=[r["effect"] for r in free], k=1)[0]
59
+
60
+
61
+ def effect_draw(rng: random.Random, model: str, pyproject: Path, taken: set[str], window):
62
+ """An explore Candidate weighted by effect, or None for a uniform draw.
63
+
64
+ It takes one draw from ``rng`` for the share, and one more if it weights.
65
+ """
66
+ from openinterp.planner import SKILLS, Candidate, _audit_options
67
+
68
+ path = table_path(pyproject.parent, model)
69
+ if rng.random() >= effect_share(pyproject) or not path.exists():
70
+ return None
71
+ row = weighted_pole(rng, model, json.loads(path.read_text())["poles"], taken)
72
+ if row is None:
73
+ return None
74
+ audit = (
75
+ f"open-interpretability audit --model {model} --layer {row['layer']} "
76
+ f"--neuron {row['neuron']} --pole {row['pole']} --selection attribution"
77
+ ) + (f" --window-tokens {window}" if window else "")
78
+ return Candidate(
79
+ kind="explore",
80
+ model=model,
81
+ weight=0.0,
82
+ rationale=f"new pole drawn by its estimated loss effect ({row['effect']:.2e}); "
83
+ "selection attribution, so it doesn't count towards registry statistics",
84
+ commands=[audit],
85
+ slug=unit_slug(model, row),
86
+ skills=SKILLS["explore"],
87
+ layer=row["layer"],
88
+ audit=_audit_options([audit]),
89
+ )
@@ -0,0 +1,92 @@
1
+ """The unit of an explore draw: a random neuron pole or a random attention head (#3248).
2
+
3
+ Before this, explore draws always sampled an MLP neuron pole. So heads were audited only
4
+ by hand, and the registry had no head records for follow-ups to start from. Now, after the
5
+ model draw, an explore draw becomes a random head audit with probability ``head_share``.
6
+
7
+ The share is configured (``[tool.openinterp] head_share``), not weighted by unit count.
8
+ Qwen2.5-0.5B has 336 heads and about 233,000 poles. By count, heads would be under 0.2%
9
+ of draws, so they would almost never come up.
10
+
11
+ The kind is drawn from a generator seeded by the audit seed, not from the planner's own.
12
+ So the planner's sequence of draws doesn't shift. A seed that drew a neuron still draws
13
+ that neuron, unless it falls in the head share.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import random
19
+ from pathlib import Path
20
+ from typing import TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from openinterp.planner import Candidate
24
+
25
+ DEFAULT_HEAD_SHARE = 0.1 # of explore draws; [tool.openinterp] head_share overrides it
26
+ _KIND_SALT = 0x4EAD # mixes the audit seed into the kind draw's own generator
27
+
28
+
29
+ def head_share(pyproject: Path = Path("pyproject.toml")) -> float:
30
+ """Return ``[tool.openinterp] head_share``, or the default."""
31
+ from openinterp.planner import _config
32
+
33
+ return float(_config(pyproject).get("head_share", DEFAULT_HEAD_SHARE))
34
+
35
+
36
+ def explore_audit(model: str, seed: int, window: int | None, unit: str | None = None) -> str:
37
+ """The ``open-interpretability audit`` command of a random explore draw."""
38
+ audit = f"open-interpretability audit --model {model} --random --seed {seed}"
39
+ if unit:
40
+ audit += f" --unit {unit}"
41
+ if window:
42
+ audit += f" --window-tokens {window}"
43
+ return audit
44
+
45
+
46
+ def is_head_draw(audit_seed: int, share: float) -> bool:
47
+ """Whether the explore draw with ``audit_seed`` studies a head, at ``share``."""
48
+ return random.Random(audit_seed ^ _KIND_SALT).random() < share
49
+
50
+
51
+ def head_draw(model: str, audit_seed: int, pyproject: Path, window: int | None) -> Candidate | None:
52
+ """A random head audit of ``model``, or None when the draw stays a neuron pole."""
53
+ from openinterp.planner import Candidate, _audit_options
54
+
55
+ share = head_share(pyproject)
56
+ if not is_head_draw(audit_seed, share):
57
+ return None
58
+ audit = explore_audit(model, audit_seed, window, unit="head")
59
+ return Candidate(
60
+ kind="explore",
61
+ model=model,
62
+ weight=0.0,
63
+ rationale=f"uniformly random attention head (unbiased among heads); heads get "
64
+ f"{share:.0%} of explore draws ([tool.openinterp] head_share)",
65
+ commands=[audit],
66
+ skills=["attention-head-audit"], # the explore skills are for neuron poles
67
+ audit=_audit_options([audit]),
68
+ )
69
+
70
+
71
+ def neuron_draw(
72
+ model: str, audit_seed: int, window: int | None, chance: float | None, why: str
73
+ ) -> Candidate:
74
+ """A random neuron-pole audit of ``model``: the default explore draw.
75
+
76
+ ``chance`` is the model's probability in the model draw, or None with one model.
77
+ """
78
+ from openinterp.planner import SKILLS, Candidate, _audit_options
79
+
80
+ rationale = "uniformly random new neuron (unbiased; counts towards registry statistics)"
81
+ if chance is not None:
82
+ rationale += f"; {model} drawn with probability {chance:.0%} by {why}"
83
+ audit = explore_audit(model, audit_seed, window)
84
+ return Candidate(
85
+ kind="explore",
86
+ model=model,
87
+ weight=0.0,
88
+ rationale=rationale,
89
+ commands=[audit],
90
+ skills=SKILLS["explore"],
91
+ audit=_audit_options([audit]),
92
+ )
@@ -0,0 +1,174 @@
1
+ """Which model an explore draw studies: the smallest one below its coverage target.
2
+
3
+ One fully interpretable model is worth more than a little coverage of many. So the
4
+ models are ordered by size (MLP neurons, ``layers × width``). The smallest one whose
5
+ verified share is below the target gets most of the explore draws: the *focus*. A
6
+ neuron is verified when the latest record of one of its poles is VERIFIED. A pole that
7
+ the weights prove dead also counts (``openinterp.harness.dead``). When the focus passes
8
+ the target, the focus moves to the next model. The other models share
9
+ the rest of the draws, ``∝ 1/√(1 + neurons covered)``, so sister draws and cross-model
10
+ comparisons still happen. Neurons within a model are still drawn at random, so each
11
+ model's statistics stay unbiased.
12
+
13
+ A neuron is covered when it has a record or an open PR claims it. A model of unknown
14
+ size can't be the focus. When no model can be, every model is weighted by coverage.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import math
21
+ import re
22
+ from collections.abc import Iterable, Sequence
23
+ from dataclasses import dataclass
24
+
25
+ from openinterp.core.identifiers import NeuronIdentifier, model_slug
26
+ from openinterp.core.schemas import AuditRecord, ComponentIdentifier
27
+
28
+ # The focus moves on when this share of its neurons is verified.
29
+ DEFAULT_COVERAGE_TARGET = 0.8
30
+ # The share of explore draws left for the other models. ``[tool.openinterp]`` can set both.
31
+ DEFAULT_OTHERS_SHARE = 0.2
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ModelCoverage:
36
+ """How much of one model's MLP the registry and open claims cover."""
37
+
38
+ model: str
39
+ covered: int # neurons with a record, a stopped scan or an open claim
40
+ total: int | None # every MLP neuron (layers × width); None when unknown
41
+ verified: int = 0 # neurons with a VERIFIED pole
42
+
43
+ @property
44
+ def verified_share(self) -> float | None:
45
+ """Verified share of the model's neurons, or None when its size is unknown."""
46
+ return self.verified / self.total if self.total else None
47
+
48
+ def __str__(self) -> str:
49
+ """``319 covered, 62/36,864 verified (0.17%)``; no total when the size is unknown."""
50
+ if not self.total:
51
+ return f"{self.covered:,} covered, {self.verified:,} verified"
52
+ return (
53
+ f"{self.covered:,} covered, {self.verified:,}/{self.total:,} verified "
54
+ f"({self.verified_share:.2%})"
55
+ )
56
+
57
+
58
+ def model_config(model: str, download: bool = True) -> dict | None:
59
+ """The model's ``config.json``, or None if it can't be read.
60
+
61
+ With ``download`` False, read only the Hugging Face cache: no network. The file is in
62
+ the cache once the model has been used. Reading it never loads weights.
63
+ """
64
+ from huggingface_hub import hf_hub_download, try_to_load_from_cache
65
+
66
+ try:
67
+ path = hf_hub_download(model, "config.json") if download else None
68
+ path = path or try_to_load_from_cache(model, "config.json")
69
+ if not isinstance(path, str): # not cached, or cached as missing
70
+ return None
71
+ with open(path) as fh:
72
+ return json.load(fh)
73
+ except Exception: # offline with nothing cached, or an unknown model
74
+ return None
75
+
76
+
77
+ def model_dims(model: str, download: bool = True) -> tuple[int, int] | None:
78
+ """(layers, MLP width) from the model's ``config.json``, or None if it can't be read.
79
+
80
+ ``download`` is as for :func:`model_config`.
81
+ """
82
+ config = model_config(model, download)
83
+ if config is None:
84
+ return None
85
+ return config_dims(config)
86
+
87
+
88
+ def model_heads(model: str, download: bool = True) -> tuple[int, int] | None:
89
+ """(layers, attention heads per layer) from ``config.json``, or None if it can't be read."""
90
+ config = model_config(model, download)
91
+ if config is None:
92
+ return None
93
+ layers = config.get("num_hidden_layers") or config.get("n_layer")
94
+ heads = config.get("num_attention_heads") or config.get("n_head")
95
+ return (layers, heads) if layers and heads else None
96
+
97
+
98
+ def config_dims(config: dict) -> tuple[int, int] | None:
99
+ """(layers, MLP width) from a ``config.json``, or None if it does not give them."""
100
+ layers = config.get("num_hidden_layers") or config.get("n_layer")
101
+ width = config.get("intermediate_size") or config.get("n_inner")
102
+ if width is None and config.get("n_embd"): # GPT-2: n_inner null means 4 × n_embd
103
+ width = 4 * config["n_embd"]
104
+ return (layers, width) if layers and width else None
105
+
106
+
107
+ def model_coverage(
108
+ pool: Sequence[str],
109
+ records: Iterable[AuditRecord],
110
+ claims: Sequence[str],
111
+ dead: Iterable[ComponentIdentifier] = (),
112
+ stopped: Iterable[ComponentIdentifier] = (),
113
+ ) -> dict[str, ModelCoverage]:
114
+ """Coverage of each model in ``pool``.
115
+
116
+ ``records`` are the latest record of each pole. A neuron is verified when one of
117
+ them is VERIFIED. A pole in ``dead`` (proven dead by the weights) also counts. A pole
118
+ in ``stopped`` (a scan stopped on it: dead or default-on) counts as covered. The
119
+ size comes from the records (``model_num_layers × model_mlp_width``), else from the
120
+ model's cached ``config.json``.
121
+ """
122
+ neurons: dict[str, set[tuple[int, int]]] = {m: set() for m in pool}
123
+ verified: dict[str, set[tuple[int, int]]] = {m: set() for m in pool}
124
+ dims: dict[str, tuple[int, int]] = {}
125
+ for record in records:
126
+ sub, ident = record.submission, record.submission.identifier
127
+ if ident.model_name not in neurons or not isinstance(ident, NeuronIdentifier):
128
+ continue
129
+ neurons[ident.model_name].add((ident.layer, ident.neuron_idx))
130
+ if record.verdict.status == "VERIFIED":
131
+ verified[ident.model_name].add((ident.layer, ident.neuron_idx))
132
+ if sub.model_num_layers and sub.model_mlp_width:
133
+ dims[ident.model_name] = (sub.model_num_layers, sub.model_mlp_width)
134
+ for ident in dead:
135
+ if ident.model_name in neurons and isinstance(ident, NeuronIdentifier):
136
+ neurons[ident.model_name].add((ident.layer, ident.neuron_idx))
137
+ verified[ident.model_name].add((ident.layer, ident.neuron_idx))
138
+ for ident in stopped:
139
+ if ident.model_name in neurons and isinstance(ident, NeuronIdentifier):
140
+ neurons[ident.model_name].add((ident.layer, ident.neuron_idx))
141
+ coverage = {}
142
+ for model in pool:
143
+ claim = re.compile(rf"{re.escape(model_slug(model))}-L(\d+)-N(\d+)-") # see claim_key
144
+ for c in claims:
145
+ neurons[model].update((int(m[1]), int(m[2])) for m in claim.finditer(c))
146
+ layers_width = dims.get(model) or model_dims(model, download=False)
147
+ total = layers_width[0] * layers_width[1] if layers_width else None
148
+ coverage[model] = ModelCoverage(model, len(neurons[model]), total, len(verified[model]))
149
+ return coverage
150
+
151
+
152
+ def focus_model(coverage: Iterable[ModelCoverage], target: float) -> str | None:
153
+ """The smallest model of known size whose verified share is below ``target``, if any."""
154
+ below = [c for c in coverage if c.total and c.verified / c.total < target]
155
+ return min(below, key=lambda c: (c.total, c.model)).model if below else None
156
+
157
+
158
+ def explore_weights(
159
+ coverage: dict[str, ModelCoverage], target: float, others_share: float
160
+ ) -> dict[str, float]:
161
+ """Explore probability per model: ``1 − others_share`` for the focus model.
162
+
163
+ The other models share ``others_share``, ``∝ 1/√(1 + neurons covered)``.
164
+ """
165
+
166
+ def by_coverage(models: Sequence[str]) -> dict[str, float]:
167
+ weights = {m: 1.0 / math.sqrt(1 + coverage[m].covered) for m in models}
168
+ return {m: w / sum(weights.values()) for m, w in weights.items()}
169
+
170
+ focus = focus_model(coverage.values(), target)
171
+ if focus is None or len(coverage) == 1:
172
+ return by_coverage(list(coverage))
173
+ others = by_coverage([m for m in coverage if m != focus])
174
+ return {focus: 1.0 - others_share, **{m: others_share * w for m, w in others.items()}}
@@ -0,0 +1,122 @@
1
+ """``sister`` draws: VERIFIED poles' explanations tested in other models.
2
+
3
+ ``open-interpretability next`` makes these draws.
4
+ :func:`openinterp.planner.plan_next` adds these to the follow-up candidates. The weights
5
+ are in docs/PROTOCOL.md (Sister study).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Sequence
11
+ from pathlib import Path
12
+
13
+ from openinterp.planner import (
14
+ BASE_WEIGHTS,
15
+ SKILLS,
16
+ Candidate,
17
+ _decay,
18
+ claim_key,
19
+ configured_windows,
20
+ )
21
+
22
+ # A match of a VERIFIED pole that isn't its sister can be drawn too. Its weight is this
23
+ # share of a sister's weight, times its correlation. Any link can be tested. But the
24
+ # draws lean heavily towards the sisters, where the explanation is likeliest to carry over.
25
+ LINK_WEIGHT = 0.05
26
+ # So can any pole of another configured model, found by the search or not. For each
27
+ # VERIFIED pole and model, one draw picks a random pole there. Its weight is this share
28
+ # of a sister's weight.
29
+ # It is also the control for the matched draws.
30
+ UNMATCHED_WEIGHT = 0.01
31
+
32
+
33
+ def link_affinity(row, match) -> float:
34
+ """Return how strongly to draw a study of ``match``.
35
+
36
+ 1 for the pole's sister, else :data:`LINK_WEIGHT` × its correlation. Never zero, as
37
+ for every weight here.
38
+ """
39
+ return 1.0 if row.sister is match else LINK_WEIGHT * max(match.pearson, 0.01)
40
+
41
+
42
+ def sister_candidates(
43
+ states, claims, now, pool: Sequence[str], root: Path, seed: int = 0
44
+ ) -> list[Candidate]:
45
+ """Return the ``sister`` draws for the models of ``pool``.
46
+
47
+ A sister draw audits a pole of another model with a VERIFIED pole's explanation,
48
+ unchanged. The draws are every unstudied match that ``open-interpretability sisters`` found
49
+ (read from ``root``), weighted by :func:`link_affinity`. Each model also gets a
50
+ random pole (:data:`UNMATCHED_WEIGHT`, seeded from ``seed``). Only models in the pool
51
+ are drawn, so CI can reproduce the record.
52
+ """
53
+ from openinterp.sisters import DEFAULT_PATH, load_rows
54
+
55
+ rows, windows = load_rows(root / DEFAULT_PATH), configured_windows(root / "pyproject.toml")
56
+ out = []
57
+ for slug, state in states.items():
58
+ ident = state.record.submission.identifier
59
+ if state.record.verdict.status != "VERIFIED" or ident.kind != "mlp_neuron_pole":
60
+ continue
61
+ for row in rows.get(slug, []):
62
+ for m in row.matches if row.other in pool else []:
63
+ if m.slug(row.other) not in states:
64
+ out.append(_sister(state, row, m, windows.get(row.other), claims, now))
65
+ for other in pool:
66
+ if other != ident.model_name:
67
+ out.append(_unmatched(state, other, windows.get(other), seed, claims, now))
68
+ return out
69
+
70
+
71
+ def _unmatched(state, other: str, window: int | None, seed: int, claims, now) -> Candidate:
72
+ """The draw that audits a random pole of ``other`` with ``state``'s explanation."""
73
+ import hashlib
74
+
75
+ digest = hashlib.sha256(f"{seed} {state.slug} {other}".encode()).digest()
76
+ audit = f"--model {other} --random --seed {int.from_bytes(digest[:4])}"
77
+ if window:
78
+ audit += f" --window-tokens {window}"
79
+ audit += f" --sister-of {state.record_path.as_posix()}"
80
+ factor, notes = _decay(state, claims, now)
81
+ why = (
82
+ f"a random pole of {other}, matched by no search, with {state.slug}'s explanation: "
83
+ "the control for its matches"
84
+ )
85
+ verify = "open-interpretability verify --audit-file <new submission> --corpus <scan corpus>"
86
+ return Candidate(
87
+ kind="sister",
88
+ model=other,
89
+ weight=BASE_WEIGHTS["sister"] * UNMATCHED_WEIGHT * factor,
90
+ rationale="; ".join([why, *notes]),
91
+ commands=[f"open-interpretability audit {audit}", verify],
92
+ skills=SKILLS["sister"],
93
+ record=state.record_path.as_posix(),
94
+ audit=audit,
95
+ )
96
+
97
+
98
+ def _sister(state, row, m, window: int | None, claims, now) -> Candidate:
99
+ """The draw that audits ``m`` with ``state``'s explanation."""
100
+ target = m.slug(row.other)
101
+ factor, notes = _decay(state, claims, now)
102
+ if any(claim_key(target) in c for c in claims):
103
+ factor, notes = factor * 0.05, [*notes, "an open PR already claims the match"]
104
+ audit = f"--model {row.other} --layer {m.layer} --neuron {m.neuron_idx} --pole {m.pole}"
105
+ if window:
106
+ audit += f" --window-tokens {window}"
107
+ audit += f" --sister-of {state.record_path.as_posix()}"
108
+ link = "sister" if row.sister is m else "correlated match (not a sister)"
109
+ why = f"{link} of {state.slug} (r {m.pearson:+.2f}): does its explanation carry over?"
110
+ verify = "open-interpretability verify --audit-file <new submission> --corpus <scan corpus>"
111
+ return Candidate(
112
+ kind="sister",
113
+ model=row.other,
114
+ weight=BASE_WEIGHTS["sister"] * link_affinity(row, m) * factor,
115
+ rationale="; ".join([why, *notes]),
116
+ commands=[f"open-interpretability audit {audit}", verify],
117
+ slug=target,
118
+ skills=SKILLS["sister"],
119
+ layer=m.layer,
120
+ record=state.record_path.as_posix(),
121
+ audit=audit,
122
+ )
@@ -0,0 +1,57 @@
1
+ """What the planner knows of each studied unit: its latest counted record and its history.
2
+
3
+ :func:`load_states` reads the archive with the errata of each record applied
4
+ (:mod:`openinterp.core.errata`, issue #3271). So the planner sees the standings of the site.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+
13
+ from openinterp.core.agent_names import canonical_agent
14
+ from openinterp.core.schemas import AuditRecord
15
+
16
+
17
+ @dataclass
18
+ class NeuronState:
19
+ """What the registry holds on one unit: its latest record and how much it has been studied."""
20
+
21
+ slug: str
22
+ record: AuditRecord
23
+ record_path: Path
24
+ experiments: int = 0
25
+ reviewers: set[str] = field(default_factory=set)
26
+ independent_reviews: int = 0 # experiments reviewed by a model other than the proposer's
27
+ last: datetime | None = None
28
+
29
+
30
+ def load_states(wiki_dir: Path) -> dict[str, NeuronState]:
31
+ """One state per neuron pole, derived from all of its archived experiments.
32
+
33
+ Each record has the edits of its errata applied (:mod:`openinterp.core.errata`).
34
+ Follow-ups are planned from the most recent experiment that counts toward the status.
35
+ An experiment that an erratum sets aside doesn't count. The exception is a pole whose
36
+ experiments are all set aside, as in the standing rule. The count, reviewers and recency
37
+ cover every experiment on the neuron.
38
+ """
39
+ from openinterp.builder.wiki import load_archive_paths # Jinja: keep the CLI start light
40
+ from openinterp.core.errata import corrected, load_errata
41
+
42
+ errata = load_errata(wiki_dir)
43
+ states: dict[str, NeuronState] = {}
44
+ counted: set[str] = set() # poles with an experiment that isn't set aside
45
+ for path, raw in load_archive_paths(Path(wiki_dir)): # oldest first
46
+ record, aside = corrected(raw, errata)
47
+ slug = record.submission.identifier.slug
48
+ state = states.setdefault(slug, NeuronState(slug, record, path))
49
+ if not aside or slug not in counted:
50
+ state.record, state.record_path = record, path
51
+ if not aside:
52
+ counted.add(slug)
53
+ state.experiments += 1
54
+ state.reviewers.add(canonical_agent(record.verdict.reviewer_agent_model))
55
+ state.independent_reviews += not record.self_reviewed
56
+ state.last = record.verdict.timestamp
57
+ return states
@@ -0,0 +1,114 @@
1
+ """``superposition`` draws: audits of the superpositions defined in ``data/superpositions/``.
2
+
3
+ A superposition is a sparse direction over one layer's MLP neurons (issue #3215).
4
+ ``open-interpretability superpositions`` writes the definitions, one per line (issue #3686).
5
+ :func:`openinterp.planner.plan_next` adds a draw for each definition that has no
6
+ experiment and no claim (issue #3687).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections.abc import Sequence
13
+ from pathlib import Path
14
+
15
+ from openinterp.core.paths import DEFAULT_EXCHANGE_DIR, DEFAULT_WIKI_DIR
16
+ from openinterp.planner import BASE_WEIGHTS, SKILLS, Candidate, claim_key, configured_windows
17
+
18
+ DEFINITIONS_DIR = Path("data/superpositions")
19
+ SHOW_COMMAND = "open-interpretability superpositions show"
20
+ # A superposition draw has the weight of one sister draw (the full sister weight, not
21
+ # LINK_WEIGHT or UNMATCHED_WEIGHT). Both test a ready unit that no experiment has studied yet.
22
+ SUPERPOSITION_WEIGHT = BASE_WEIGHTS["sister"]
23
+
24
+
25
+ def load_definitions(path: Path) -> list[dict]:
26
+ """Return the definitions in the JSONL file ``path``, one per non-empty line."""
27
+ lines = path.read_text(encoding="utf-8").splitlines()
28
+ return [json.loads(line) for line in lines if line.strip()]
29
+
30
+
31
+ def definition_identifier(definition: dict):
32
+ """Return the :class:`SuperpositionIdentifier` of one definition."""
33
+ from openinterp.core.identifiers import SuperpositionIdentifier
34
+ from openinterp.core.profiles import SuperpositionProfile, superposition_id
35
+
36
+ fields = {k: v for k, v in definition.items() if k not in ("model", "layer")}
37
+ profile = SuperpositionProfile.model_validate(fields)
38
+ return SuperpositionIdentifier(
39
+ model_name=definition["model"],
40
+ layer=definition["layer"],
41
+ superposition_id=superposition_id(profile.members),
42
+ )
43
+
44
+
45
+ def default_definitions(model: str) -> Path:
46
+ """Return the file that holds ``model``'s definitions, relative to the repo root."""
47
+ from openinterp.core.identifiers import model_slug
48
+
49
+ return DEFINITIONS_DIR / f"{model_slug(model)}.jsonl"
50
+
51
+
52
+ def definition_file(slug: str) -> str:
53
+ """Return the path where a draw of the superposition ``slug`` writes its definition."""
54
+ return f"{DEFAULT_EXCHANGE_DIR}/{claim_key(slug)}/superposition.json"
55
+
56
+
57
+ def superposition_candidates(states, claims: Sequence[str], root: Path) -> list[Candidate]:
58
+ """Return a ``superposition`` draw for each unstudied definition under ``root``.
59
+
60
+ The definitions are in every ``root/data/superpositions/*.jsonl``. A definition is
61
+ left out when ``states`` has an experiment on it, or when an open PR in ``claims``
62
+ claims it. It is also left out when a scan of it stopped (``wiki/stops/``): it is
63
+ dead or default-on, and no audit can follow. A missing folder gives no draws.
64
+ """
65
+ stopped = _stopped(root / DEFAULT_WIKI_DIR)
66
+ folder = root / DEFINITIONS_DIR
67
+ files = sorted(folder.glob("*.jsonl")) if folder.is_dir() else []
68
+ windows = configured_windows(root / "pyproject.toml")
69
+ out = []
70
+ for path in files:
71
+ for definition in load_definitions(path):
72
+ ident = definition_identifier(definition)
73
+ if ident.slug in states or ident.slug in stopped:
74
+ continue
75
+ if any(claim_key(ident.slug) in c for c in claims):
76
+ continue
77
+ source = path.relative_to(root)
78
+ out.append(_candidate(ident, definition, source, windows.get(ident.model_name)))
79
+ return out
80
+
81
+
82
+ def _stopped(wiki_dir: Path) -> set[str]:
83
+ """The slugs of the chosen units whose scans stopped."""
84
+ from openinterp.core.draws import load_stops
85
+
86
+ return {s.identifier.slug for s in load_stops(wiki_dir)} if wiki_dir.is_dir() else set()
87
+
88
+
89
+ def _candidate(ident, definition: dict, source: Path, window: int | None) -> Candidate:
90
+ """The draw that audits the superposition ``ident``, defined in the file ``source``."""
91
+ target = definition_file(ident.slug)
92
+ show = f"{SHOW_COMMAND} {ident.superposition_id} --model {ident.model_name} --out {target}"
93
+ if source != default_definitions(ident.model_name):
94
+ show += f" --definitions {source.as_posix()}"
95
+ audit = f"--superposition {target}"
96
+ if window:
97
+ audit += f" --window-tokens {window}"
98
+ members = len(definition["members"])
99
+ why = (
100
+ f"an unstudied superposition of {members} neurons on layer {ident.layer} "
101
+ f"({definition['method']}, from {definition['source']})"
102
+ )
103
+ verify = "open-interpretability verify --audit-file <new submission> --corpus <scan corpus>"
104
+ return Candidate(
105
+ kind="superposition",
106
+ model=ident.model_name,
107
+ weight=SUPERPOSITION_WEIGHT,
108
+ rationale=why,
109
+ commands=[show, f"open-interpretability audit {audit}", verify],
110
+ slug=ident.slug,
111
+ skills=SKILLS["superposition"],
112
+ layer=ident.layer,
113
+ audit=audit,
114
+ )