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,495 @@
|
|
|
1
|
+
"""``open-interpretability audit`` and its helpers: scan caching and prompt/answer files.
|
|
2
|
+
|
|
3
|
+
Corpus loading and the scan itself are in :mod:`openinterp.evidence`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from openinterp.audit_options import ( # noqa: F401 (re-exported)
|
|
13
|
+
open_scan_cache,
|
|
14
|
+
read_superposition,
|
|
15
|
+
resolve_unit,
|
|
16
|
+
scan_cache_option,
|
|
17
|
+
selection_option,
|
|
18
|
+
)
|
|
19
|
+
from openinterp.cli import (
|
|
20
|
+
cli,
|
|
21
|
+
contributor_option,
|
|
22
|
+
recorded_model_option,
|
|
23
|
+
wiki_dir_option,
|
|
24
|
+
with_model_options,
|
|
25
|
+
)
|
|
26
|
+
from openinterp.corpus import DEFAULT_SAMPLE_SEED, DEFAULT_SAMPLE_TOKENS
|
|
27
|
+
from openinterp.evidence import ( # noqa: F401 (re-exported)
|
|
28
|
+
ScanRequest,
|
|
29
|
+
Unauditable,
|
|
30
|
+
read_corpus,
|
|
31
|
+
scan_evidence,
|
|
32
|
+
sha256,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
REGISTRY_CORPUS = Path("corpus.jsonl")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_superseded(path: Path):
|
|
39
|
+
"""The archived, reviewed record that ``audit --supersedes`` names."""
|
|
40
|
+
from openinterp.core.schemas import AuditRecord
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
record = AuditRecord.model_validate_json(path.read_text())
|
|
44
|
+
except ValueError as exc:
|
|
45
|
+
raise click.BadParameter(f"not an audit record: {exc}", param_hint="--supersedes") from exc
|
|
46
|
+
if record.verdict is None:
|
|
47
|
+
raise click.BadParameter("that record has no verdict yet", param_hint="--supersedes")
|
|
48
|
+
return record
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def answer_path(prompt_path: Path, suffix: str) -> Path:
|
|
52
|
+
"""``review.prompt.md`` or ``review.md`` -> ``review<suffix>``."""
|
|
53
|
+
name = prompt_path.name.removesuffix(".md").removesuffix(".prompt")
|
|
54
|
+
return prompt_path.with_name(name + suffix)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def with_costs(provenance, *parts):
|
|
58
|
+
"""``provenance`` with ``parts`` merged into its costs; unchanged if it is ``None``."""
|
|
59
|
+
from openinterp.core.costs import merge
|
|
60
|
+
|
|
61
|
+
if provenance is None:
|
|
62
|
+
return None
|
|
63
|
+
return provenance.model_copy(update={"costs": merge(provenance.costs, *parts)})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def record_draw(exc: Unauditable, random_pick: bool, wiki_dir: Path) -> None:
|
|
67
|
+
"""Keep a unit the scan stopped on, so that the registry shows the attempt.
|
|
68
|
+
|
|
69
|
+
Random-selection statistics count a random draw. A chosen unit gets a stop record and a
|
|
70
|
+
page instead, which those statistics do not read.
|
|
71
|
+
"""
|
|
72
|
+
from openinterp.builder.wiki import write_stop_page
|
|
73
|
+
from openinterp.cli import _skip_wiki
|
|
74
|
+
from openinterp.core.draws import write_draw
|
|
75
|
+
|
|
76
|
+
if exc.draw is None or _skip_wiki(wiki_dir):
|
|
77
|
+
return
|
|
78
|
+
outcome = exc.draw.outcome.replace("_", "-")
|
|
79
|
+
if random_pick:
|
|
80
|
+
click.echo(f"Recorded the {outcome} draw: {write_draw(exc.draw, wiki_dir)}", err=True)
|
|
81
|
+
else:
|
|
82
|
+
click.echo(f"Recorded the {outcome} stop: {write_stop_page(exc.draw, wiki_dir)}", err=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def proposer_context(evidence, wiki_dir: Path, sisters: bool, sister_file=None, superseded=None):
|
|
86
|
+
"""Return what other research links to the unit, for the proposer.
|
|
87
|
+
|
|
88
|
+
That is the unit's studied sisters, and the record a revision supersedes. In a sister
|
|
89
|
+
study (``sister_file``), it is only the study's brief, because the explanation is fixed.
|
|
90
|
+
"""
|
|
91
|
+
from openinterp.cli_review import sisters_text
|
|
92
|
+
from openinterp.revision import previous_attempt
|
|
93
|
+
from openinterp.sisters import sister_brief
|
|
94
|
+
|
|
95
|
+
if sister_file:
|
|
96
|
+
return sister_brief(load_sister(sister_file, evidence.identifier))
|
|
97
|
+
related = sisters_text(evidence.identifier, wiki_dir, "proposer") if sisters else ""
|
|
98
|
+
return related + (previous_attempt(superseded) if superseded else "")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_sister(path: Path, unit):
|
|
102
|
+
"""The record ``audit --sister-of`` names, checked against the audited ``unit``."""
|
|
103
|
+
from openinterp.sisters import load_sister_source
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
return load_sister_source(path, unit)
|
|
107
|
+
except ValueError as exc:
|
|
108
|
+
raise click.BadParameter(str(exc), param_hint="--sister-of") from exc
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def write_evidence(evidence, path: Path, full_snippets: bool = False, related: str = "") -> None:
|
|
112
|
+
"""Write ``evidence`` to ``path`` with this run's costs, and the proposer prompt beside it."""
|
|
113
|
+
from openinterp.agents.proposer import HypothesisDraft, ProposerAgent
|
|
114
|
+
from openinterp.core import costs
|
|
115
|
+
|
|
116
|
+
evidence = evidence.model_copy(
|
|
117
|
+
update={"provenance": with_costs(evidence.provenance, costs.measured())}
|
|
118
|
+
)
|
|
119
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
path.write_text(evidence.model_dump_json(indent=2))
|
|
121
|
+
prompt_path = path.with_suffix(".prompt.md")
|
|
122
|
+
answer_path = path.with_suffix(".hypothesis.json")
|
|
123
|
+
task = ProposerAgent.proposal_prompt(
|
|
124
|
+
evidence.identifier,
|
|
125
|
+
evidence.top_examples,
|
|
126
|
+
evidence.vocabulary_lens,
|
|
127
|
+
evidence.confounds,
|
|
128
|
+
evidence.correlated_neurons,
|
|
129
|
+
evidence.head_profile,
|
|
130
|
+
evidence.input_attribution,
|
|
131
|
+
evidence.dictionary_profile,
|
|
132
|
+
bool(evidence.first_token_floor),
|
|
133
|
+
full_snippets,
|
|
134
|
+
related,
|
|
135
|
+
superposition_profile=evidence.superposition_profile,
|
|
136
|
+
manifold=evidence.manifold,
|
|
137
|
+
)
|
|
138
|
+
prompt_path.write_text(ProposerAgent.render_prompt(HypothesisDraft, task, answer_path))
|
|
139
|
+
click.echo(f"Unit: {evidence.identifier}")
|
|
140
|
+
click.echo(f"Wrote {path}")
|
|
141
|
+
click.echo(f"Proposer prompt: {prompt_path} (answer in {answer_path})")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@cli.command()
|
|
145
|
+
@click.option(
|
|
146
|
+
"--model",
|
|
147
|
+
"model_name",
|
|
148
|
+
help="Hugging Face repo, e.g. Qwen/Qwen2.5-0.5B. Required unless --evidence.",
|
|
149
|
+
)
|
|
150
|
+
@click.option("--layer", type=int, help="Required unless --random.")
|
|
151
|
+
@click.option("--neuron", type=int, help="Required unless --random or --head.")
|
|
152
|
+
@click.option("--pole", type=click.Choice(["positive", "negative"]), help="Default: positive.")
|
|
153
|
+
@click.option(
|
|
154
|
+
"--head",
|
|
155
|
+
type=int,
|
|
156
|
+
help="Audit this attention head of --layer instead of a neuron (implies --unit head).",
|
|
157
|
+
)
|
|
158
|
+
@click.option(
|
|
159
|
+
"--feature",
|
|
160
|
+
type=int,
|
|
161
|
+
help="Audit this latent of --dictionary on --layer instead of a neuron (implies --unit "
|
|
162
|
+
"feature).",
|
|
163
|
+
)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--dictionary",
|
|
166
|
+
help="Sparse dictionary (transcoder or SAE) whose latent to audit, e.g. "
|
|
167
|
+
"gpt2-small-transcoders or gemma-scope-2b-mlp-saes; it implies --model. See "
|
|
168
|
+
"openinterp/harness/dictionaries.py.",
|
|
169
|
+
)
|
|
170
|
+
@click.option(
|
|
171
|
+
"--superposition",
|
|
172
|
+
"superposition_file",
|
|
173
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
174
|
+
help="Audit the superposition that this JSON file defines: {model, layer, members: "
|
|
175
|
+
"[{neuron_idx, weight}], method, source}. It implies --model and --layer.",
|
|
176
|
+
)
|
|
177
|
+
@click.option(
|
|
178
|
+
"--unit",
|
|
179
|
+
type=click.Choice(["neuron", "head", "feature"]),
|
|
180
|
+
help="What --random samples: an MLP neuron pole (default), an attention head, or a "
|
|
181
|
+
"latent of --dictionary (the learned-basis baseline).",
|
|
182
|
+
)
|
|
183
|
+
@click.option(
|
|
184
|
+
"--random",
|
|
185
|
+
"random_pick",
|
|
186
|
+
is_flag=True,
|
|
187
|
+
help="Sample unspecified layer/neuron/pole (or head) uniformly: the unbiased selection.",
|
|
188
|
+
)
|
|
189
|
+
@click.option("--seed", type=int, help="Seed for --random (recorded in provenance).")
|
|
190
|
+
@click.option(
|
|
191
|
+
"--corpus",
|
|
192
|
+
type=click.Path(exists=True, path_type=Path),
|
|
193
|
+
help="Text file (one snippet per line) or .jsonl with a 'text' field. "
|
|
194
|
+
"Default: ./corpus.jsonl, the registry's scan corpus (see the README).",
|
|
195
|
+
)
|
|
196
|
+
@click.option(
|
|
197
|
+
"--max-tokens",
|
|
198
|
+
default=DEFAULT_SAMPLE_TOKENS,
|
|
199
|
+
show_default=True,
|
|
200
|
+
type=click.IntRange(0),
|
|
201
|
+
help="Scan a seeded sample of whole documents with at most this many tokens, so the "
|
|
202
|
+
"scan's cost and scale don't grow with the corpus (recorded in provenance). "
|
|
203
|
+
"0 scans every document.",
|
|
204
|
+
)
|
|
205
|
+
@click.option(
|
|
206
|
+
"--sample-seed",
|
|
207
|
+
default=DEFAULT_SAMPLE_SEED,
|
|
208
|
+
show_default=True,
|
|
209
|
+
type=int,
|
|
210
|
+
help="Seed for the --max-tokens sample. Keep the default so audits of one corpus read "
|
|
211
|
+
"the same documents.",
|
|
212
|
+
)
|
|
213
|
+
@click.option("--top-k", default=20, show_default=True, help="Examples shown to the proposer.")
|
|
214
|
+
@click.option(
|
|
215
|
+
"--full-snippets",
|
|
216
|
+
is_flag=True,
|
|
217
|
+
help="Show the proposer whole scanned documents, without windowing or dropping "
|
|
218
|
+
"near-duplicates: the prompt before trimming, for A/B comparison.",
|
|
219
|
+
)
|
|
220
|
+
@click.option(
|
|
221
|
+
"--window-tokens",
|
|
222
|
+
type=click.IntRange(2),
|
|
223
|
+
help="Scan each document as overlapping windows of this many tokens (≥ 512 recommended) "
|
|
224
|
+
"instead of whole. Top examples are then windows. Default: whole documents.",
|
|
225
|
+
)
|
|
226
|
+
@click.option(
|
|
227
|
+
"--stride",
|
|
228
|
+
type=click.IntRange(1),
|
|
229
|
+
help="Tokens between window starts (default: half of --window-tokens). Each token counts "
|
|
230
|
+
"once, with at least --window-tokens minus --stride tokens of context.",
|
|
231
|
+
)
|
|
232
|
+
@scan_cache_option
|
|
233
|
+
@click.option(
|
|
234
|
+
"--agent-model",
|
|
235
|
+
default=None,
|
|
236
|
+
callback=recorded_model_option,
|
|
237
|
+
help="Proposer. LiteLLM model string, or external/<name> to answer a prompt file. "
|
|
238
|
+
"With --hypothesis, the name recorded for whoever wrote it (e.g. external/claude-opus-5-5).",
|
|
239
|
+
)
|
|
240
|
+
@contributor_option("proposer")
|
|
241
|
+
@click.option(
|
|
242
|
+
"--evidence-out",
|
|
243
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
244
|
+
help="Scan only: write the evidence to this JSON file and the proposer's prompt next to "
|
|
245
|
+
"it (<name>.prompt.md), then exit. Answer the prompt, then run with --evidence.",
|
|
246
|
+
)
|
|
247
|
+
@click.option(
|
|
248
|
+
"--evidence",
|
|
249
|
+
"evidence_file",
|
|
250
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
251
|
+
help="Evidence from --evidence-out; skips the scan. Requires --hypothesis.",
|
|
252
|
+
)
|
|
253
|
+
@click.option(
|
|
254
|
+
"--hypothesis",
|
|
255
|
+
"hypothesis_file",
|
|
256
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
257
|
+
help="HypothesisDraft JSON answering the --evidence-out prompt. No LLM is called.",
|
|
258
|
+
)
|
|
259
|
+
@click.option(
|
|
260
|
+
"--sister-of",
|
|
261
|
+
"sister_file",
|
|
262
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
263
|
+
help="A sister-neuron study: the archived VERIFIED record of a pole in another model "
|
|
264
|
+
"whose explanation this pole is tested on, unchanged. Give it to both steps.",
|
|
265
|
+
)
|
|
266
|
+
@click.option(
|
|
267
|
+
"--sisters/--no-sisters",
|
|
268
|
+
default=True,
|
|
269
|
+
show_default=True,
|
|
270
|
+
help="Show the proposer the pole's sister neurons in other models that this registry "
|
|
271
|
+
"has studied (`open-interpretability sisters`), with their explanations.",
|
|
272
|
+
)
|
|
273
|
+
@click.option(
|
|
274
|
+
"--supersedes",
|
|
275
|
+
"superseded_file",
|
|
276
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
277
|
+
help="Archived record (wiki/experiments/<slug>/<id>.json) this submission revises, e.g. a "
|
|
278
|
+
"NEEDS_REVISION one. Recorded in the submission; it must be on the same unit.",
|
|
279
|
+
)
|
|
280
|
+
@click.option(
|
|
281
|
+
"--out", type=click.Path(path_type=Path), help="Output JSON (default: submissions/<slug>.json)"
|
|
282
|
+
)
|
|
283
|
+
@wiki_dir_option(
|
|
284
|
+
"Registry root. A --random draw the scan stops on (dead or default-on) is recorded "
|
|
285
|
+
"in draws/<slug>/, as an outcome of random selection. A chosen unit is recorded in "
|
|
286
|
+
"stops/<slug>/. Pass --wiki-dir '' to skip.",
|
|
287
|
+
)
|
|
288
|
+
@selection_option
|
|
289
|
+
@with_model_options
|
|
290
|
+
def audit( # long-function: a click command; ScanRequest takes one line per option
|
|
291
|
+
model_name,
|
|
292
|
+
layer,
|
|
293
|
+
neuron,
|
|
294
|
+
pole,
|
|
295
|
+
head,
|
|
296
|
+
feature,
|
|
297
|
+
dictionary,
|
|
298
|
+
superposition_file,
|
|
299
|
+
unit,
|
|
300
|
+
random_pick,
|
|
301
|
+
seed,
|
|
302
|
+
corpus,
|
|
303
|
+
max_tokens,
|
|
304
|
+
sample_seed,
|
|
305
|
+
top_k,
|
|
306
|
+
full_snippets,
|
|
307
|
+
window_tokens,
|
|
308
|
+
stride,
|
|
309
|
+
use_scan_cache,
|
|
310
|
+
selection,
|
|
311
|
+
agent_model,
|
|
312
|
+
contributor,
|
|
313
|
+
evidence_out,
|
|
314
|
+
evidence_file,
|
|
315
|
+
hypothesis_file,
|
|
316
|
+
sister_file,
|
|
317
|
+
sisters,
|
|
318
|
+
superseded_file,
|
|
319
|
+
out,
|
|
320
|
+
wiki_dir,
|
|
321
|
+
device,
|
|
322
|
+
dtype,
|
|
323
|
+
):
|
|
324
|
+
"""Scan a unit, get a hypothesis, and write a submission.
|
|
325
|
+
|
|
326
|
+
The unit is a neuron pole, an attention head, a dictionary feature or a superposition.
|
|
327
|
+
|
|
328
|
+
\b
|
|
329
|
+
In one step, the proposer is an LLM (or external/<name>, answered through exchange/).
|
|
330
|
+
In two steps, any agent can propose without an API key or a waiting process:
|
|
331
|
+
open-interpretability audit --model M --random --evidence-out ev.json
|
|
332
|
+
(answer ev.prompt.md by writing ev.hypothesis.json)
|
|
333
|
+
open-interpretability audit --evidence ev.json --hypothesis ev.hypothesis.json \\
|
|
334
|
+
--agent-model external/<model that wrote it>
|
|
335
|
+
"""
|
|
336
|
+
unit = resolve_unit(unit, neuron, pole, head, feature, dictionary)
|
|
337
|
+
superposition = None
|
|
338
|
+
if superposition_file is not None:
|
|
339
|
+
unit, model_name, layer, superposition = read_superposition(superposition_file, unit)
|
|
340
|
+
if unit == "feature" and evidence_file is None:
|
|
341
|
+
model_name = check_dictionary(dictionary, model_name, window_tokens)
|
|
342
|
+
check_steps(evidence_file, hypothesis_file, evidence_out, superseded_file, agent_model)
|
|
343
|
+
check_scan(evidence_file, model_name, stride, window_tokens)
|
|
344
|
+
superseded = superseded_file and load_superseded(superseded_file)
|
|
345
|
+
if evidence_file is not None:
|
|
346
|
+
evidence, draft, runner, agent = load_answer(
|
|
347
|
+
evidence_file, hypothesis_file, agent_model, device, dtype
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
request = ScanRequest(
|
|
351
|
+
model_name=model_name,
|
|
352
|
+
unit=unit,
|
|
353
|
+
layer=layer,
|
|
354
|
+
neuron=neuron,
|
|
355
|
+
pole=pole,
|
|
356
|
+
head=head,
|
|
357
|
+
feature=feature,
|
|
358
|
+
dictionary=dictionary,
|
|
359
|
+
superposition=superposition,
|
|
360
|
+
random_pick=random_pick,
|
|
361
|
+
seed=seed,
|
|
362
|
+
corpus=scan_corpus(corpus),
|
|
363
|
+
top_k=top_k,
|
|
364
|
+
dtype=dtype,
|
|
365
|
+
window_tokens=window_tokens,
|
|
366
|
+
stride=stride,
|
|
367
|
+
max_tokens=max_tokens,
|
|
368
|
+
sample_seed=sample_seed,
|
|
369
|
+
selection=selection,
|
|
370
|
+
)
|
|
371
|
+
runner, evidence = scan_or_record(request, use_scan_cache, wiki_dir, device)
|
|
372
|
+
related = proposer_context(evidence, wiki_dir, sisters, sister_file, superseded)
|
|
373
|
+
if evidence_out is not None:
|
|
374
|
+
write_evidence(evidence, evidence_out, full_snippets, related)
|
|
375
|
+
return
|
|
376
|
+
agent, draft = ask_proposer(agent_model, evidence, full_snippets, related)
|
|
377
|
+
from openinterp.cli_submission import write_submission
|
|
378
|
+
|
|
379
|
+
write_submission(agent, draft, runner, evidence, superseded, out, contributor, sister_file)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def check_dictionary(dictionary, model_name, window_tokens) -> str:
|
|
383
|
+
"""The model ``--dictionary`` is for, after checking it fits the scan's windows."""
|
|
384
|
+
from openinterp.harness.dictionaries import dictionary_spec
|
|
385
|
+
|
|
386
|
+
if dictionary is None:
|
|
387
|
+
raise click.UsageError("--unit feature needs --dictionary.")
|
|
388
|
+
try:
|
|
389
|
+
spec = dictionary_spec(dictionary)
|
|
390
|
+
except ValueError as exc:
|
|
391
|
+
raise click.BadParameter(str(exc), param_hint="--dictionary") from exc
|
|
392
|
+
if model_name not in (None, spec.model_name):
|
|
393
|
+
raise click.UsageError(f"{dictionary} is a dictionary for {spec.model_name}.")
|
|
394
|
+
limit = spec.context_tokens
|
|
395
|
+
if limit and (window_tokens is None or window_tokens > limit):
|
|
396
|
+
raise click.UsageError(
|
|
397
|
+
f"{dictionary} only fits contexts of up to {limit} tokens; "
|
|
398
|
+
f"scan with --window-tokens {limit}."
|
|
399
|
+
)
|
|
400
|
+
return spec.model_name
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def check_steps(evidence_file, hypothesis_file, evidence_out, superseded_file, agent_model):
|
|
404
|
+
"""Options that go together, or apart, across the one- and two-step audits."""
|
|
405
|
+
if (evidence_file is None) != (hypothesis_file is None):
|
|
406
|
+
raise click.UsageError("--evidence and --hypothesis go together.")
|
|
407
|
+
if superseded_file is not None and evidence_out is not None:
|
|
408
|
+
raise click.UsageError(
|
|
409
|
+
"--supersedes goes on the step that writes the submission (with --hypothesis)."
|
|
410
|
+
)
|
|
411
|
+
if evidence_file is not None and evidence_out is not None:
|
|
412
|
+
raise click.UsageError("--evidence-out scans; --evidence reuses a scan. Pick one.")
|
|
413
|
+
if evidence_file is not None and not agent_model:
|
|
414
|
+
raise click.UsageError(
|
|
415
|
+
"--agent-model is required with --hypothesis: name the model that wrote it."
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def check_scan(evidence_file, model_name, stride, window_tokens) -> None:
|
|
420
|
+
"""Reject scan options that do not go together, before any model loads."""
|
|
421
|
+
if stride is not None and (window_tokens is None or stride > window_tokens):
|
|
422
|
+
raise click.UsageError("--stride needs --window-tokens, and can't exceed it.")
|
|
423
|
+
if evidence_file is None and model_name is None:
|
|
424
|
+
raise click.UsageError("--model is required unless --evidence is given.")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def scan_corpus(corpus: Path | None) -> Path:
|
|
428
|
+
"""Return the corpus to scan: ``--corpus``, else the registry's ``corpus.jsonl``.
|
|
429
|
+
|
|
430
|
+
Never the built-in sample, whose ~552 tokens make a scan that says little.
|
|
431
|
+
"""
|
|
432
|
+
if corpus is not None:
|
|
433
|
+
return corpus
|
|
434
|
+
if REGISTRY_CORPUS.exists():
|
|
435
|
+
click.echo(f"Scanning {REGISTRY_CORPUS} (no --corpus given).", err=True)
|
|
436
|
+
return REGISTRY_CORPUS
|
|
437
|
+
raise click.UsageError(
|
|
438
|
+
f"No --corpus given and no {REGISTRY_CORPUS} here. Build the registry's scan corpus "
|
|
439
|
+
"with `open-interpretability corpus` (see the README), copy it in, or pass --corpus."
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def load_answer(evidence_file, hypothesis_file, agent_model, device, dtype):
|
|
444
|
+
"""Step two: the evidence, the external proposer's draft, the model and the agent."""
|
|
445
|
+
from openinterp.agents.proposer import HypothesisDraft, ProposerAgent
|
|
446
|
+
from openinterp.cli import _load_runner # looked up per call, so tests can swap it
|
|
447
|
+
from openinterp.core import costs
|
|
448
|
+
from openinterp.core.schemas import ScanEvidence
|
|
449
|
+
from openinterp.harness.units import attn_implementation
|
|
450
|
+
|
|
451
|
+
evidence = ScanEvidence.model_validate_json(evidence_file.read_text())
|
|
452
|
+
draft = HypothesisDraft.model_validate_json(hypothesis_file.read_text())
|
|
453
|
+
prompt_file = evidence_file.with_suffix(".prompt.md")
|
|
454
|
+
costs.add_external("proposer", agent_model, prompt_file, hypothesis_file)
|
|
455
|
+
attn = attn_implementation(evidence.identifier)
|
|
456
|
+
runner = _load_runner(evidence.identifier.model_name, device, dtype, None, attn)
|
|
457
|
+
return evidence, draft, runner, ProposerAgent(agent_model)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def scan_or_record(request, use_scan_cache: bool, wiki_dir: Path, device):
|
|
461
|
+
"""Load the model and scan; keep a unit the scan stops on (see record_draw)."""
|
|
462
|
+
from openinterp.cli import _load_runner # looked up per call, so tests can swap it
|
|
463
|
+
|
|
464
|
+
attn = "eager" if request.unit == "head" else None # heads need attention patterns
|
|
465
|
+
runner = _load_runner(request.model_name, device, request.dtype, None, attn)
|
|
466
|
+
try:
|
|
467
|
+
return runner, scan_evidence(runner, request, open_scan_cache(use_scan_cache))
|
|
468
|
+
except Unauditable as exc:
|
|
469
|
+
record_draw(exc, request.random_pick, wiki_dir)
|
|
470
|
+
raise
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def ask_proposer(agent_model, evidence, full_snippets: bool, related: str = ""):
|
|
474
|
+
"""Ask the LLM proposer for a hypothesis about ``evidence``'s unit."""
|
|
475
|
+
from openinterp.agents.base import DEFAULT_MODEL
|
|
476
|
+
from openinterp.agents.proposer import ProposerAgent
|
|
477
|
+
|
|
478
|
+
agent = ProposerAgent(agent_model or DEFAULT_MODEL)
|
|
479
|
+
click.echo(f"Asking {agent.model} for a hypothesis…", err=True)
|
|
480
|
+
draft = agent.propose(
|
|
481
|
+
evidence.identifier,
|
|
482
|
+
evidence.top_examples,
|
|
483
|
+
evidence.vocabulary_lens,
|
|
484
|
+
evidence.confounds,
|
|
485
|
+
evidence.correlated_neurons,
|
|
486
|
+
evidence.head_profile,
|
|
487
|
+
evidence.input_attribution,
|
|
488
|
+
evidence.dictionary_profile,
|
|
489
|
+
bool(evidence.first_token_floor),
|
|
490
|
+
full_snippets,
|
|
491
|
+
related,
|
|
492
|
+
superposition_profile=evidence.superposition_profile,
|
|
493
|
+
manifold=evidence.manifold,
|
|
494
|
+
)
|
|
495
|
+
return agent, draft
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""``open-interpretability evidence``: move slim records' blobs to the dataset (issue #3254).
|
|
2
|
+
|
|
3
|
+
``push`` runs where the experiment ran. It pushes the blobs of the records that the branch
|
|
4
|
+
adds to ``evidence/<branch>``. CI runs ``upload`` (with ``HF_EVIDENCE_TOKEN``) or ``load``
|
|
5
|
+
(without it), then ``verify``. :mod:`openinterp.evidence_branch` explains why the blobs
|
|
6
|
+
ride on their own branch.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from openinterp.cli import cli, wiki_dir_option
|
|
17
|
+
|
|
18
|
+
base_ref_option = click.option(
|
|
19
|
+
"--base-ref", default="origin/main", show_default=True, help="The branch the PR merges into."
|
|
20
|
+
)
|
|
21
|
+
ref_option = click.option(
|
|
22
|
+
"--ref", required=True, help="The evidence branch, for example FETCH_HEAD after a fetch."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _added(wiki_dir: Path, base_ref: str) -> list[Path]:
|
|
27
|
+
from openinterp.checks import changed_records
|
|
28
|
+
|
|
29
|
+
return changed_records(wiki_dir, base_ref)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cli.group()
|
|
33
|
+
def evidence() -> None:
|
|
34
|
+
"""Carry slim records' evidence blobs to the public dataset (docs/analyses/repo-size.md)."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@evidence.command()
|
|
38
|
+
@wiki_dir_option()
|
|
39
|
+
@base_ref_option
|
|
40
|
+
@click.option("--remote", default="origin", show_default=True)
|
|
41
|
+
def push(wiki_dir: Path, base_ref: str, remote: str) -> None:
|
|
42
|
+
"""Push the blobs of the records this branch adds to evidence/<branch>."""
|
|
43
|
+
from openinterp.evidence_branch import push as push_blobs
|
|
44
|
+
from openinterp.evidence_branch import record_refs
|
|
45
|
+
|
|
46
|
+
shas = record_refs(_added(wiki_dir, base_ref))
|
|
47
|
+
if not shas:
|
|
48
|
+
click.echo("No slim record on this branch: nothing to push.")
|
|
49
|
+
return
|
|
50
|
+
branch = subprocess.run(
|
|
51
|
+
["git", "branch", "--show-current"], capture_output=True, text=True, check=True
|
|
52
|
+
).stdout.strip()
|
|
53
|
+
target = push_blobs(branch, shas, remote)
|
|
54
|
+
click.echo(f"Pushed {len(shas)} evidence blobs to {remote}/{target}.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@evidence.command()
|
|
58
|
+
@ref_option
|
|
59
|
+
def load(ref: str) -> None:
|
|
60
|
+
"""Copy an evidence branch's blobs into the local store, checking each hash."""
|
|
61
|
+
from openinterp.evidence_branch import load_branch
|
|
62
|
+
|
|
63
|
+
click.echo(f"Loaded {len(load_branch(ref))} evidence blobs from {ref}.")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@evidence.command()
|
|
67
|
+
@ref_option
|
|
68
|
+
def upload(ref: str) -> None:
|
|
69
|
+
"""Load an evidence branch's blobs, then upload them to the dataset (needs a write token)."""
|
|
70
|
+
from openinterp.core.evidence import EVIDENCE_REPO
|
|
71
|
+
from openinterp.evidence_branch import load_branch
|
|
72
|
+
from openinterp.evidence_branch import upload as upload_blobs
|
|
73
|
+
|
|
74
|
+
shas = load_branch(ref)
|
|
75
|
+
click.echo(f"Uploaded {upload_blobs(shas)} evidence blobs to {EVIDENCE_REPO}.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@evidence.command()
|
|
79
|
+
@wiki_dir_option()
|
|
80
|
+
@base_ref_option
|
|
81
|
+
def verify(wiki_dir: Path, base_ref: str) -> None:
|
|
82
|
+
"""Fail unless each record this branch adds has its evidence, with the right hashes."""
|
|
83
|
+
from openinterp.evidence_branch import unresolved
|
|
84
|
+
|
|
85
|
+
problems = unresolved(_added(wiki_dir, base_ref))
|
|
86
|
+
for problem in problems:
|
|
87
|
+
click.echo(problem, err=True)
|
|
88
|
+
if problems:
|
|
89
|
+
raise click.ClickException(
|
|
90
|
+
f"{len(problems)} records lack their evidence. Run `open-interpretability evidence "
|
|
91
|
+
"push` on the branch, then run the checks again."
|
|
92
|
+
)
|
|
93
|
+
click.echo("Every added record has its evidence.")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Attribute one answer of a model to its labelled neuron poles.
|
|
2
|
+
|
|
3
|
+
The command is ``open-interpretability explain``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from dataclasses import asdict
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from openinterp.cli import _load_runner, cli, wiki_dir_option, with_model_options
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@cli.command(name="explain")
|
|
18
|
+
@click.argument("prompt")
|
|
19
|
+
@click.option("--model", "model_name", default="Qwen/Qwen2.5-0.5B", show_default=True)
|
|
20
|
+
@click.option("--token", help="The answer token to explain. Default: the model's top token.")
|
|
21
|
+
@click.option("--position", default=-1, show_default=True, help="The position to explain.")
|
|
22
|
+
@click.option("--top", default=20, show_default=True, help="Poles to list.")
|
|
23
|
+
@click.option(
|
|
24
|
+
"--corpus", default="corpus.jsonl", show_default=True, type=click.Path(path_type=Path)
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--mean-tokens",
|
|
28
|
+
default=20_000,
|
|
29
|
+
show_default=True,
|
|
30
|
+
help="Tokens of the corpus for each neuron's mean, the attribution's baseline (cached).",
|
|
31
|
+
)
|
|
32
|
+
@click.option("--json", "as_json", is_flag=True, help="Print the result as JSON.")
|
|
33
|
+
@wiki_dir_option()
|
|
34
|
+
@with_model_options
|
|
35
|
+
def explain_command(
|
|
36
|
+
prompt, model_name, token, position, top, corpus, mean_tokens, as_json, wiki_dir, device, dtype
|
|
37
|
+
):
|
|
38
|
+
"""Explain the model's answer to PROMPT as fully as the registry allows.
|
|
39
|
+
|
|
40
|
+
It attributes the answer's logit to every MLP neuron pole, against each neuron's mean
|
|
41
|
+
on the corpus. It labels the top poles from the registry. It reports the share of
|
|
42
|
+
the attribution that VERIFIED poles hold, and checks it by mean-ablating them.
|
|
43
|
+
"""
|
|
44
|
+
from openinterp.evidence import read_corpus
|
|
45
|
+
from openinterp.explain import explain, neuron_means, registry_labels
|
|
46
|
+
|
|
47
|
+
runner = _load_runner(model_name, device, dtype)
|
|
48
|
+
means = neuron_means(runner, model_name, read_corpus(corpus)[0], mean_tokens)
|
|
49
|
+
labels = registry_labels(Path(wiki_dir), model_name)
|
|
50
|
+
target = None
|
|
51
|
+
if token is not None:
|
|
52
|
+
ids = runner.tokenizer.encode(token, add_special_tokens=False)
|
|
53
|
+
if len(ids) != 1:
|
|
54
|
+
raise click.BadParameter(
|
|
55
|
+
f"{token!r} is {len(ids)} tokens, not one", param_hint="--token"
|
|
56
|
+
)
|
|
57
|
+
target = ids[0]
|
|
58
|
+
result = explain(
|
|
59
|
+
runner, model_name, prompt, means, labels, token=target, position=position, top=top
|
|
60
|
+
)
|
|
61
|
+
if as_json:
|
|
62
|
+
click.echo(json.dumps(asdict(result) | {"verified_share": result.verified_share}, indent=1))
|
|
63
|
+
return
|
|
64
|
+
_print(result)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _print(result) -> None:
|
|
68
|
+
click.echo(f"Answer at position {result.position}: {result.token!r} (logit {result.logit:.2f})")
|
|
69
|
+
click.echo(
|
|
70
|
+
f"{'pole':<14} {'attribution':>11} {'share':>6} {'ablated Δ':>9} {'peak/max':>9} status"
|
|
71
|
+
)
|
|
72
|
+
for p in result.poles:
|
|
73
|
+
share = abs(p.attribution) / result.total if result.total else 0.0
|
|
74
|
+
ablated = f"{p.ablation_change:+.2f}" if p.ablation_change is not None else ""
|
|
75
|
+
peak = f"{p.strength / p.max_activation:.2f}" if p.max_activation else "—"
|
|
76
|
+
row = f"{p.label:<14} {p.attribution:>+11.3f} {share:>6.1%} {ablated:>9} {peak:>9}"
|
|
77
|
+
click.echo(f"{row} {p.status}")
|
|
78
|
+
if p.explanation:
|
|
79
|
+
click.echo(f"{'':<14} {p.explanation[:110]}")
|
|
80
|
+
click.echo(
|
|
81
|
+
f"VERIFIED poles hold {result.verified_share:.1%} of the total |attribution|; "
|
|
82
|
+
f"{1 - result.verified_share:.1%} is unexplained."
|
|
83
|
+
)
|
|
84
|
+
joint = result.joint
|
|
85
|
+
if joint.get("poles"):
|
|
86
|
+
changed = (
|
|
87
|
+
f"the top token changes to {joint['new_top_token']!r}"
|
|
88
|
+
if joint["top_token_changed"]
|
|
89
|
+
else "the top token stays"
|
|
90
|
+
)
|
|
91
|
+
click.echo(
|
|
92
|
+
f"Mean-ablating the {joint['poles']} VERIFIED poles above together moves the answer's "
|
|
93
|
+
f"logit by {joint['logit_change']:+.2f}, and {changed}."
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
click.echo("No pole above is VERIFIED, so nothing here is explained yet.")
|
|
97
|
+
click.echo(
|
|
98
|
+
"Attribution is first order and against each neuron's mean. The ablated Δ column is "
|
|
99
|
+
"the exact check: mean-ablating a pole moves the logit by about minus its attribution. "
|
|
100
|
+
"Unverified poles carry no label."
|
|
101
|
+
)
|