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.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- 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
|
+
)
|