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,357 @@
|
|
|
1
|
+
"""``open-interpretability loop``: run experiments back to back with API agents (issue #3246).
|
|
2
|
+
|
|
3
|
+
Each iteration resets a detached worktree to ``origin/main`` and draws with the planner.
|
|
4
|
+
It claims the draw as ``choose-next-experiment`` does, then runs the draw's steps
|
|
5
|
+
(``draw_steps.steps``, the commands ``open-interpretability next --steps`` prints). Then it
|
|
6
|
+
posts the result, and lands it as the ``land-experiment`` skill does.
|
|
7
|
+
``open-interpretability`` commands run in this process, so a model loads once
|
|
8
|
+
(``runner_cache``). git and gh run as subprocesses.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import shlex
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from openinterp.core.paths import DEFAULT_WIKI_DIR, draws_dir, experiments_dir, stops_dir
|
|
20
|
+
from openinterp.draw_steps import REVIEW_KINDS, VERBS, draw_slug, steps
|
|
21
|
+
from openinterp.planner import claim_key
|
|
22
|
+
|
|
23
|
+
MERGE_PERMISSIONS = ("ADMIN", "MAINTAIN", "WRITE")
|
|
24
|
+
CHECKS_TIMEOUT = 45 * 60 # seconds; the land-experiment skill gives up after 45 minutes
|
|
25
|
+
OPENINTERP = ("uv", "run", "open-interpretability")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LoopError(Exception):
|
|
29
|
+
"""One iteration failed. Its message is the reason that the loop gives."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Options:
|
|
34
|
+
"""What every iteration of a loop shares."""
|
|
35
|
+
|
|
36
|
+
proposer: str
|
|
37
|
+
reviewer: str
|
|
38
|
+
epsilon: float = 0.7
|
|
39
|
+
corpus: str | None = None # None: each new scan builds a fresh corpus from its seed
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Result:
|
|
44
|
+
"""The outcome of one iteration, as the RESULT line reports it."""
|
|
45
|
+
|
|
46
|
+
slug: str | None
|
|
47
|
+
verdict: str
|
|
48
|
+
url: str | None = None
|
|
49
|
+
merged: bool = False
|
|
50
|
+
|
|
51
|
+
def line(self) -> str:
|
|
52
|
+
"""``RESULT: <slug> <verdict> <PR url> <merged|open>``, or ``RESULT: none <why>``."""
|
|
53
|
+
if self.slug is None:
|
|
54
|
+
return f"RESULT: none {self.verdict}"
|
|
55
|
+
return (
|
|
56
|
+
f"RESULT: {self.slug} {self.verdict} {self.url} {'merged' if self.merged else 'open'}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def sh(args: list[str], check: bool = True, timeout: float | None = None) -> str:
|
|
61
|
+
"""Run ``args`` in the current directory and return its stdout; echo both streams."""
|
|
62
|
+
try:
|
|
63
|
+
done = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
|
|
64
|
+
except subprocess.TimeoutExpired as exc:
|
|
65
|
+
raise LoopError(f"`{' '.join(args[:3])}` timed out") from exc
|
|
66
|
+
sys.stdout.write(f"$ {shlex.join(args)}\n{done.stdout}{done.stderr}")
|
|
67
|
+
if check and done.returncode != 0:
|
|
68
|
+
tail = (done.stderr or done.stdout).strip().splitlines()[-1:] or ["no output"]
|
|
69
|
+
raise LoopError(f"`{' '.join(args[:3])}` failed: {tail[0]}")
|
|
70
|
+
return done.stdout.strip()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def openinterp(args: list[str]) -> None:
|
|
74
|
+
"""Run one ``open-interpretability`` command in this process.
|
|
75
|
+
|
|
76
|
+
A non-zero exit raises LoopError.
|
|
77
|
+
"""
|
|
78
|
+
from openinterp.cli import cli
|
|
79
|
+
|
|
80
|
+
sys.stdout.write(f"$ openinterp {shlex.join(args)}\n")
|
|
81
|
+
try:
|
|
82
|
+
cli.main(args=args, prog_name="open-interpretability", standalone_mode=False)
|
|
83
|
+
except SystemExit as exc:
|
|
84
|
+
if exc.code not in (0, None):
|
|
85
|
+
raise LoopError(f"`openinterp {args[0]}` exited {exc.code}") from exc
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_command(command: str) -> None:
|
|
89
|
+
"""Run one command of a step: ``uv run openinterp …`` in-process, anything else in a shell."""
|
|
90
|
+
args = shlex.split(command)
|
|
91
|
+
if tuple(args[:3]) == OPENINTERP:
|
|
92
|
+
openinterp(args[3:])
|
|
93
|
+
else:
|
|
94
|
+
sh(args)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def reset() -> None:
|
|
98
|
+
"""Detach the current worktree at a fresh ``origin/main`` and drop old experiment branches."""
|
|
99
|
+
sh(["git", "fetch", "--quiet", "origin", "main"])
|
|
100
|
+
sh(["git", "checkout", "--quiet", "--force", "--detach", "origin/main"])
|
|
101
|
+
sh(["git", "clean", "-fdq"])
|
|
102
|
+
branches = sh(["git", "for-each-ref", "--format=%(refname:short)", "refs/heads/experiment/"])
|
|
103
|
+
for branch in branches.split():
|
|
104
|
+
sh(["git", "branch", "-D", branch])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def proposer_of(record_path: str | None) -> str | None:
|
|
108
|
+
"""The proposer model of the record at ``record_path``, if there is one."""
|
|
109
|
+
from openinterp.core.schemas import AuditRecord
|
|
110
|
+
|
|
111
|
+
if not record_path or not Path(record_path).exists():
|
|
112
|
+
return None
|
|
113
|
+
record = AuditRecord.model_validate_json(Path(record_path).read_text())
|
|
114
|
+
return record.submission.submitting_agent_model
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def draw(options: Options, plan_next, claims):
|
|
118
|
+
"""Draw with the planner. A review the reviewer can't do independently is drawn again once.
|
|
119
|
+
|
|
120
|
+
``plan_next`` and ``claims`` are passed in so that tests can replace them.
|
|
121
|
+
"""
|
|
122
|
+
for _ in range(2):
|
|
123
|
+
plan = plan_next(DEFAULT_WIKI_DIR, epsilon=options.epsilon, claims=claims())
|
|
124
|
+
c = plan.choice
|
|
125
|
+
if c.kind not in REVIEW_KINDS or proposer_of(c.record) != options.reviewer:
|
|
126
|
+
return plan
|
|
127
|
+
raise LoopError(f"twice drew a review of {options.reviewer}'s own record")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def pr_url() -> str:
|
|
131
|
+
"""The URL of the pull request for the branch that is checked out."""
|
|
132
|
+
return sh(["gh", "pr", "view", "--json", "url", "-q", ".url"])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def new_files(prefix: Path) -> list[Path]:
|
|
136
|
+
"""Files under ``prefix`` that git does not track yet."""
|
|
137
|
+
out = sh(["git", "ls-files", "--others", "--exclude-standard", str(prefix)])
|
|
138
|
+
return [Path(line) for line in out.splitlines() if line]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def verdict_of(kind: str, key: str) -> tuple[str, str]:
|
|
142
|
+
"""The new record's verdict and the commit message that posts it.
|
|
143
|
+
|
|
144
|
+
A scan that stopped on its unit writes a draw or stop record and no experiment, so
|
|
145
|
+
its verdict is STOPPED.
|
|
146
|
+
"""
|
|
147
|
+
from openinterp.core.schemas import AuditRecord
|
|
148
|
+
|
|
149
|
+
records = [p for p in new_files(experiments_dir(DEFAULT_WIKI_DIR)) if p.suffix == ".json"]
|
|
150
|
+
if not records:
|
|
151
|
+
if new_files(draws_dir(DEFAULT_WIKI_DIR)) or new_files(stops_dir(DEFAULT_WIKI_DIR)):
|
|
152
|
+
return "STOPPED", f"{VERBS[kind]} {key} (STOPPED)"
|
|
153
|
+
raise LoopError("the draw's steps wrote no record")
|
|
154
|
+
verdict = AuditRecord.model_validate_json(records[0].read_text()).verdict
|
|
155
|
+
accuracy = verdict.metrics.balanced_accuracy if verdict.metrics else None
|
|
156
|
+
shown = "n/a" if accuracy is None else f"{accuracy:.2f}"
|
|
157
|
+
return verdict.status, f"{VERBS[kind]} {key} ({verdict.status}, {shown})"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def post(kind: str, key: str) -> str:
|
|
161
|
+
"""Write the pages, check the registry, commit ``wiki/``, push and mark the PR ready."""
|
|
162
|
+
openinterp(["wiki"])
|
|
163
|
+
openinterp(["check", "--base-ref", "origin/main"])
|
|
164
|
+
status, message = verdict_of(kind, key)
|
|
165
|
+
sh(["git", "add", str(DEFAULT_WIKI_DIR)])
|
|
166
|
+
sh(["git", "commit", "--quiet", "-m", message])
|
|
167
|
+
sh(["git", "push", "--quiet"])
|
|
168
|
+
sh(["gh", "pr", "ready"])
|
|
169
|
+
return status
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def merge_blockers(view: dict, permission: str) -> list[str]:
|
|
173
|
+
"""Why the PR described by ``view`` (``gh pr view --json``) may not be merged, if at all."""
|
|
174
|
+
files = [f["path"] for f in view.get("files", [])]
|
|
175
|
+
wiki = f"{DEFAULT_WIKI_DIR}/"
|
|
176
|
+
reasons = [f"it changes {p}, outside {wiki}" for p in files if not p.startswith(wiki)]
|
|
177
|
+
if permission not in MERGE_PERMISSIONS:
|
|
178
|
+
reasons.append(f"the token's permission is {permission or 'unknown'}")
|
|
179
|
+
if view.get("isDraft"):
|
|
180
|
+
reasons.append("it is a draft")
|
|
181
|
+
if view.get("mergeable") != "MERGEABLE":
|
|
182
|
+
reasons.append(f"GitHub says it is {view.get('mergeable', 'not mergeable')}")
|
|
183
|
+
return reasons
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def land(url: str) -> bool:
|
|
187
|
+
"""Wait for the checks, then merge the PR if every merge condition holds. True if merged."""
|
|
188
|
+
import json
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
sh(
|
|
192
|
+
["gh", "pr", "checks", url, "--watch", "--fail-fast", "--interval", "30"],
|
|
193
|
+
timeout=CHECKS_TIMEOUT,
|
|
194
|
+
)
|
|
195
|
+
except LoopError as exc:
|
|
196
|
+
sh(["gh", "pr", "comment", url, "--body", f"Left open: the checks didn't pass ({exc})."])
|
|
197
|
+
return False
|
|
198
|
+
fields = "state,isDraft,mergeable,files,headRefOid"
|
|
199
|
+
view = json.loads(sh(["gh", "pr", "view", url, "--json", fields]))
|
|
200
|
+
if view.get("state") == "MERGED": # the auto-merge workflow got there first
|
|
201
|
+
return True
|
|
202
|
+
permission = sh(["gh", "repo", "view", "--json", "viewerPermission", "-q", ".viewerPermission"])
|
|
203
|
+
if reasons := merge_blockers(view, permission):
|
|
204
|
+
sh(["gh", "pr", "comment", url, "--body", "Left open: " + "; ".join(reasons) + "."])
|
|
205
|
+
return False
|
|
206
|
+
sh(["gh", "pr", "merge", url, "--squash", "--match-head-commit", view["headRefOid"]])
|
|
207
|
+
delete_branch()
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def delete_branch() -> None:
|
|
212
|
+
"""Delete the checked-out branch on origin, so that a later draw can claim it again."""
|
|
213
|
+
branch = sh(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
|
214
|
+
sh(["git", "push", "--quiet", "origin", "--delete", branch], check=False)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def iteration(options: Options, plan_next, claims) -> Result:
|
|
218
|
+
"""Run one experiment end to end: reset, draw, claim, run, post and land."""
|
|
219
|
+
reset()
|
|
220
|
+
plan = draw(options, plan_next, claims)
|
|
221
|
+
slug = draw_slug(plan.choice)
|
|
222
|
+
if slug is None:
|
|
223
|
+
raise LoopError(f"the unit of a {plan.choice.kind} draw on {plan.choice.model} is unknown")
|
|
224
|
+
corpus = options.corpus or "corpus.jsonl"
|
|
225
|
+
claim, *work, _ = steps(
|
|
226
|
+
plan, corpus, options.proposer, options.reviewer, options.corpus is None
|
|
227
|
+
)
|
|
228
|
+
for command in claim.commands:
|
|
229
|
+
run_command(command)
|
|
230
|
+
url = pr_url()
|
|
231
|
+
try:
|
|
232
|
+
status = run_work(work, plan.choice.kind, claim_key(slug))
|
|
233
|
+
except Exception as exc:
|
|
234
|
+
sh(
|
|
235
|
+
[
|
|
236
|
+
"gh",
|
|
237
|
+
"pr",
|
|
238
|
+
"close",
|
|
239
|
+
url,
|
|
240
|
+
"--comment",
|
|
241
|
+
f"Abandoned by `open-interpretability loop`: {exc}",
|
|
242
|
+
],
|
|
243
|
+
check=False,
|
|
244
|
+
)
|
|
245
|
+
delete_branch()
|
|
246
|
+
raise
|
|
247
|
+
return Result(slug, status, url, land(url))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def run_work(work, kind: str, key: str) -> str:
|
|
251
|
+
"""Run every step after the claim, then post. A scan that stops on its unit posts at once."""
|
|
252
|
+
from openinterp.evidence import Unauditable
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
for step in work:
|
|
256
|
+
for command in step.commands:
|
|
257
|
+
run_command(command)
|
|
258
|
+
except Unauditable as exc:
|
|
259
|
+
sys.stdout.write(f"The scan stopped on the unit: {exc}\n")
|
|
260
|
+
return post(kind, key)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
BACKOFF_MINUTES = (1, 5, 15) # waits after a provider's rate or usage limit; then stop
|
|
264
|
+
_LIMIT_WORDS = ("rate limit", "ratelimit", "usage limit", "429", "overloaded", "quota")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def is_rate_limit(exc: BaseException) -> bool:
|
|
268
|
+
"""True if ``exc``, or an exception it was raised from, is a provider's rate or usage limit."""
|
|
269
|
+
seen: BaseException | None = exc
|
|
270
|
+
while seen is not None:
|
|
271
|
+
text = f"{type(seen).__name__} {seen}".lower()
|
|
272
|
+
if any(word in text for word in _LIMIT_WORDS):
|
|
273
|
+
return True
|
|
274
|
+
seen = seen.__cause__ or seen.__context__
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def stop_file() -> Path:
|
|
279
|
+
"""The kill switch that ``scripts/experiment-loop.sh`` also honours."""
|
|
280
|
+
import os
|
|
281
|
+
|
|
282
|
+
cache = os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
|
|
283
|
+
return Path(os.environ.get("OPENINTERP_STOP_FILE") or Path(cache) / "openinterp" / "stop")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass
|
|
287
|
+
class Loop:
|
|
288
|
+
"""The loop's counters, and the pieces that tests replace."""
|
|
289
|
+
|
|
290
|
+
options: Options
|
|
291
|
+
log_dir: Path
|
|
292
|
+
max_experiments: int = 0 # 0: until stopped
|
|
293
|
+
max_failures: int = 3
|
|
294
|
+
plan_next: object = None
|
|
295
|
+
claims: object = None
|
|
296
|
+
sleep: object = None
|
|
297
|
+
done: int = 0
|
|
298
|
+
failures: int = 0
|
|
299
|
+
backoffs: int = 0
|
|
300
|
+
|
|
301
|
+
def run(self) -> int:
|
|
302
|
+
"""Run until the limit, the stop file or too many failures; return the exit code."""
|
|
303
|
+
while not self.max_experiments or self.done < self.max_experiments:
|
|
304
|
+
if stop_file().exists():
|
|
305
|
+
print(f"Stopping: {stop_file()} exists.")
|
|
306
|
+
return 0
|
|
307
|
+
self._wait_while_paused()
|
|
308
|
+
if (code := self._one()) is not None:
|
|
309
|
+
return code
|
|
310
|
+
return 0
|
|
311
|
+
|
|
312
|
+
def _one(self) -> int | None:
|
|
313
|
+
"""Run one iteration and count it. Return an exit code when the loop must stop."""
|
|
314
|
+
import contextlib
|
|
315
|
+
from datetime import UTC, datetime
|
|
316
|
+
|
|
317
|
+
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
|
318
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
log = self.log_dir / f"{stamp}.log"
|
|
320
|
+
try:
|
|
321
|
+
with (
|
|
322
|
+
open(log, "w") as out,
|
|
323
|
+
contextlib.redirect_stdout(out),
|
|
324
|
+
contextlib.redirect_stderr(out),
|
|
325
|
+
):
|
|
326
|
+
result = iteration(self.options, self.plan_next, self.claims)
|
|
327
|
+
except Exception as exc: # any failure of one experiment; the loop decides what next
|
|
328
|
+
return self._failed(exc, log)
|
|
329
|
+
self.done, self.failures, self.backoffs = self.done + 1, 0, 0
|
|
330
|
+
print(result.line(), flush=True)
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
def _failed(self, exc: Exception, log: Path) -> int | None:
|
|
334
|
+
"""Back off on a rate limit, count any other failure, and say when to stop."""
|
|
335
|
+
if is_rate_limit(exc):
|
|
336
|
+
if self.backoffs == len(BACKOFF_MINUTES):
|
|
337
|
+
print(f"RESULT: none rate limited after {self.backoffs} waits ({log})")
|
|
338
|
+
return 1
|
|
339
|
+
minutes = BACKOFF_MINUTES[self.backoffs]
|
|
340
|
+
self.backoffs += 1
|
|
341
|
+
print(f"Rate limited; waiting {minutes} min ({log})", flush=True)
|
|
342
|
+
self.sleep(minutes * 60)
|
|
343
|
+
return None
|
|
344
|
+
self.failures += 1
|
|
345
|
+
print(f"RESULT: none {exc} ({log})", flush=True)
|
|
346
|
+
return 1 if self.failures >= self.max_failures else None
|
|
347
|
+
|
|
348
|
+
def _wait_while_paused(self) -> None:
|
|
349
|
+
"""Wait, a minute at a time, while ``open-interpretability pause`` is in force."""
|
|
350
|
+
from openinterp.core import pause
|
|
351
|
+
|
|
352
|
+
said = False
|
|
353
|
+
while (paused := pause.read()) is not None and not stop_file().exists():
|
|
354
|
+
if not said:
|
|
355
|
+
print(paused.describe(), flush=True)
|
|
356
|
+
said = True
|
|
357
|
+
self.sleep(60)
|
openinterp/explain.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Explain one answer of a model as fully as the registry allows (issue #3250).
|
|
2
|
+
|
|
3
|
+
``open-interpretability explain "<prompt>"`` attributes the logit of the model's answer to every
|
|
4
|
+
MLP neuron pole. It labels the poles from the registry, and says how much of the answer is
|
|
5
|
+
explained. A sparse registry is fine: the output's job is also to show how little is
|
|
6
|
+
explained so far.
|
|
7
|
+
|
|
8
|
+
- *Attribute.* One prompt has no corrupted twin, so the baseline is each neuron's mean
|
|
9
|
+
activation over a corpus sample. At each position, a neuron's contribution is
|
|
10
|
+
(a − mean) · ∂logit/∂a. That is first order, like ``attribution_patching``. Each
|
|
11
|
+
contribution goes to the pole that the sign of ``a`` names at that position. The
|
|
12
|
+
first token, an attention sink, is left out, as scans leave it out.
|
|
13
|
+
- *Explain.* Each pole gets its newest record's status and explanation, or *stopped*
|
|
14
|
+
(a scan found it dead or default-on), or *unstudied*. Only a VERIFIED explanation is
|
|
15
|
+
shown as one.
|
|
16
|
+
- *Gaps.* The share of the total |attribution| that VERIFIED poles hold.
|
|
17
|
+
- *Check.* Mean-ablate single top poles, then the VERIFIED ones together, and measure
|
|
18
|
+
the change in the answer's logit.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
from collections.abc import Sequence
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
import torch
|
|
29
|
+
|
|
30
|
+
from openinterp.core.cache_dirs import cache_root
|
|
31
|
+
from openinterp.harness.hooks import NeuronHookRunner
|
|
32
|
+
|
|
33
|
+
MEANS_ENV = "OPENINTERP_MEANS" # the cache of per-neuron means, or "off"
|
|
34
|
+
WINDOW = 128 # tokens per window of the mean's corpus sample
|
|
35
|
+
MEANS_VERSION = 2 # in the cache key: 2 leaves each window's first token out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class PoleAttribution:
|
|
40
|
+
"""One pole's attribution to the answer, with what the registry says of it."""
|
|
41
|
+
|
|
42
|
+
layer: int
|
|
43
|
+
neuron: int
|
|
44
|
+
pole: str
|
|
45
|
+
attribution: float # summed (a − mean) · grad over the positions where it is this pole
|
|
46
|
+
strength: float # the pole's peak strength on the prompt
|
|
47
|
+
status: str = "unstudied" # VERIFIED, NEEDS_REVISION, REJECTED, stopped, unstudied
|
|
48
|
+
explanation: str | None = None
|
|
49
|
+
max_activation: float | None = None
|
|
50
|
+
ablation_change: float | None = None # target logit change when mean-ablated alone
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def label(self) -> str:
|
|
54
|
+
"""``L3/N7+``: the pole within its model."""
|
|
55
|
+
return f"L{self.layer}/N{self.neuron}{'+' if self.pole == 'positive' else '−'}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Explanation:
|
|
60
|
+
"""Everything ``open-interpretability explain`` reports for one prompt."""
|
|
61
|
+
|
|
62
|
+
prompt: str
|
|
63
|
+
position: int
|
|
64
|
+
token: str
|
|
65
|
+
logit: float
|
|
66
|
+
poles: list[PoleAttribution] # the top poles, by |attribution|
|
|
67
|
+
total: float # the sum of |attribution| over every pole of every layer
|
|
68
|
+
verified: float # the same sum over VERIFIED poles only
|
|
69
|
+
joint: dict = field(default_factory=dict) # the VERIFIED poles ablated together
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def verified_share(self) -> float:
|
|
73
|
+
"""The share of the total |attribution| that VERIFIED poles hold."""
|
|
74
|
+
return self.verified / self.total if self.total else 0.0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _layer_acts(runner: NeuronHookRunner, encoded, layers) -> dict[int, torch.Tensor]:
|
|
78
|
+
_, cache = runner.run_with_cache(encoded, layers)
|
|
79
|
+
return cache
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def neuron_means(runner: NeuronHookRunner, model: str, texts: Sequence[str], tokens: int):
|
|
83
|
+
"""Each layer's mean MLP activation per neuron, ``{layer: tensor[d_mlp]}``, cached.
|
|
84
|
+
|
|
85
|
+
The sample is the first ``tokens`` tokens of ``texts``, in windows of :data:`WINDOW`.
|
|
86
|
+
Each window's first token is left out, as the attribution leaves out the prompt's.
|
|
87
|
+
"""
|
|
88
|
+
tag = f"{model}|{tokens}|{WINDOW}|{MEANS_VERSION}|"
|
|
89
|
+
key = hashlib.sha256(tag.encode() + "\n".join(texts).encode())
|
|
90
|
+
root = cache_root(MEANS_ENV, "means")
|
|
91
|
+
path = root / f"{key.hexdigest()[:24]}.pt" if root else None
|
|
92
|
+
if path is not None and path.exists():
|
|
93
|
+
return torch.load(path)
|
|
94
|
+
ids = [i for t in texts for i in runner.tokenizer.encode(t)][:tokens]
|
|
95
|
+
windows = [ids[i : i + WINDOW] for i in range(0, len(ids) - WINDOW + 1, WINDOW)]
|
|
96
|
+
layers = list(range(runner.n_layers))
|
|
97
|
+
sums = {la: torch.zeros(runner.mlp_width(la)) for la in layers}
|
|
98
|
+
count = 0
|
|
99
|
+
for window in windows:
|
|
100
|
+
acts = _layer_acts(runner, torch.tensor([window]), layers)
|
|
101
|
+
for la in layers: # a window's first token is an attention sink: leave it out
|
|
102
|
+
sums[la] += acts[la][0, 1:].float().sum(0).cpu()
|
|
103
|
+
count += len(window) - 1
|
|
104
|
+
means = {la: s / max(count, 1) for la, s in sums.items()}
|
|
105
|
+
if path is not None:
|
|
106
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
torch.save(means, path)
|
|
108
|
+
return means
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def attribute(runner: NeuronHookRunner, prompt: str, means: dict, token: int, position: int):
|
|
112
|
+
"""Per pole, ``{(layer, neuron, pole): (attribution, peak strength)}``, and the logit.
|
|
113
|
+
|
|
114
|
+
The metric is the logit of ``token`` at ``position``. The prompt's first token is left out.
|
|
115
|
+
"""
|
|
116
|
+
layers = sorted(means)
|
|
117
|
+
captured: dict[int, torch.Tensor] = {}
|
|
118
|
+
|
|
119
|
+
def capture(layer: int):
|
|
120
|
+
def hook(act: torch.Tensor) -> None:
|
|
121
|
+
captured[layer] = act
|
|
122
|
+
|
|
123
|
+
return hook
|
|
124
|
+
|
|
125
|
+
def ensure_grad(_module, _args, output):
|
|
126
|
+
return output if output.requires_grad else output.detach().requires_grad_(True)
|
|
127
|
+
|
|
128
|
+
encoded = runner.encode(prompt)
|
|
129
|
+
handle = runner.model.get_input_embeddings().register_forward_hook(ensure_grad)
|
|
130
|
+
try:
|
|
131
|
+
with torch.enable_grad(), runner.hooks({la: capture(la) for la in layers}):
|
|
132
|
+
logit = runner.forward(encoded)[0, position, token]
|
|
133
|
+
grads = torch.autograd.grad(logit, [captured[la] for la in layers])
|
|
134
|
+
finally:
|
|
135
|
+
handle.remove()
|
|
136
|
+
out = {}
|
|
137
|
+
for la, grad in zip(layers, grads, strict=True):
|
|
138
|
+
# The first token is an attention sink, active far beyond any scan's scale whatever
|
|
139
|
+
# the prompt. The protocol leaves it out of scans, and so does this.
|
|
140
|
+
act = captured[la].detach()[0, 1:].float().cpu()
|
|
141
|
+
contribution = (act - means[la]) * grad[0, 1:].float().cpu()
|
|
142
|
+
for pole, mask in (("positive", act > 0), ("negative", act < 0)):
|
|
143
|
+
score = (contribution * mask).sum(0)
|
|
144
|
+
peak = (act.abs() * mask).amax(0)
|
|
145
|
+
for n in torch.nonzero(mask.any(0)).flatten().tolist():
|
|
146
|
+
out[(la, n, pole)] = (float(score[n]), float(peak[n]))
|
|
147
|
+
return out, float(logit.detach())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def mean_ablation(poles: Sequence[tuple[int, int, str]], means: dict):
|
|
151
|
+
"""``interventions`` that set each pole to its neuron's mean where the pole is active.
|
|
152
|
+
|
|
153
|
+
The first token keeps its value, as the attribution leaves it out.
|
|
154
|
+
"""
|
|
155
|
+
by_layer: dict[int, list[tuple[int, str]]] = {}
|
|
156
|
+
for layer, neuron, pole in poles:
|
|
157
|
+
by_layer.setdefault(layer, []).append((neuron, pole))
|
|
158
|
+
|
|
159
|
+
def at(layer: int):
|
|
160
|
+
def intervene(act: torch.Tensor) -> torch.Tensor:
|
|
161
|
+
act = act.clone()
|
|
162
|
+
for neuron, pole in by_layer[layer]:
|
|
163
|
+
a = act[..., neuron]
|
|
164
|
+
active = a > 0 if pole == "positive" else a < 0
|
|
165
|
+
active[..., 0] = False # the first token stays, as in the attribution
|
|
166
|
+
mean = means[layer][neuron].to(act.dtype)
|
|
167
|
+
act[..., neuron] = torch.where(active, mean, a)
|
|
168
|
+
return act
|
|
169
|
+
|
|
170
|
+
return intervene
|
|
171
|
+
|
|
172
|
+
return {layer: at(layer) for layer in by_layer}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@torch.no_grad()
|
|
176
|
+
def logits_with(runner: NeuronHookRunner, prompt: str, interventions, position: int):
|
|
177
|
+
"""The logits at ``position`` with ``interventions`` applied."""
|
|
178
|
+
with runner.hooks(interventions):
|
|
179
|
+
return runner.forward(runner.encode(prompt))[0, position].float().cpu()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def registry_labels(wiki_dir: Path, model: str) -> dict[str, tuple[str, str | None, float | None]]:
|
|
183
|
+
"""``{pole slug: (status, explanation, max_activation)}`` for ``model``'s poles.
|
|
184
|
+
|
|
185
|
+
The status is the newest record's verdict. A pole that a scan stopped on, with no
|
|
186
|
+
record, is ``stopped``.
|
|
187
|
+
"""
|
|
188
|
+
from openinterp.planner import load_states, stopped
|
|
189
|
+
|
|
190
|
+
out: dict[str, tuple[str, str | None, float | None]] = {}
|
|
191
|
+
for ident in stopped(wiki_dir):
|
|
192
|
+
if ident.model_name == model:
|
|
193
|
+
out[ident.slug] = ("stopped", None, None)
|
|
194
|
+
for slug, state in load_states(wiki_dir).items():
|
|
195
|
+
sub = state.record.submission
|
|
196
|
+
if sub.identifier.model_name == model:
|
|
197
|
+
out[slug] = (
|
|
198
|
+
state.record.verdict.status,
|
|
199
|
+
sub.natural_language_explanation,
|
|
200
|
+
sub.max_activation,
|
|
201
|
+
)
|
|
202
|
+
return out
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def explain(
|
|
206
|
+
runner: NeuronHookRunner,
|
|
207
|
+
model: str,
|
|
208
|
+
prompt: str,
|
|
209
|
+
means: dict,
|
|
210
|
+
labels: dict,
|
|
211
|
+
*,
|
|
212
|
+
token: int | None = None,
|
|
213
|
+
position: int = -1,
|
|
214
|
+
top: int = 20,
|
|
215
|
+
confirm: int = 10,
|
|
216
|
+
) -> Explanation:
|
|
217
|
+
"""Attribute, label, measure the VERIFIED share and check by ablation (module docstring)."""
|
|
218
|
+
from openinterp.core.identifiers import NeuronIdentifier
|
|
219
|
+
|
|
220
|
+
base = logits_with(runner, prompt, {}, position)
|
|
221
|
+
token = int(base.argmax()) if token is None else token
|
|
222
|
+
scores, logit = attribute(runner, prompt, means, token, position)
|
|
223
|
+
ranked = sorted(scores.items(), key=lambda kv: abs(kv[1][0]), reverse=True)
|
|
224
|
+
total = verified = 0.0
|
|
225
|
+
poles = []
|
|
226
|
+
for rank, ((layer, neuron, pole), (score, strength)) in enumerate(ranked):
|
|
227
|
+
slug = NeuronIdentifier(model_name=model, layer=layer, neuron_idx=neuron, pole=pole).slug
|
|
228
|
+
status, text, max_act = labels.get(slug, ("unstudied", None, None))
|
|
229
|
+
total += abs(score)
|
|
230
|
+
verified += abs(score) if status == "VERIFIED" else 0.0
|
|
231
|
+
if rank < top:
|
|
232
|
+
shown = text if status == "VERIFIED" else None
|
|
233
|
+
poles.append(
|
|
234
|
+
PoleAttribution(layer, neuron, pole, score, strength, status, shown, max_act)
|
|
235
|
+
)
|
|
236
|
+
for p in poles[:confirm]:
|
|
237
|
+
ablated = logits_with(
|
|
238
|
+
runner, prompt, mean_ablation([(p.layer, p.neuron, p.pole)], means), position
|
|
239
|
+
)
|
|
240
|
+
p.ablation_change = float(ablated[token] - base[token])
|
|
241
|
+
chosen = [(p.layer, p.neuron, p.pole) for p in poles if p.status == "VERIFIED"]
|
|
242
|
+
joint = {"poles": len(chosen)}
|
|
243
|
+
if chosen:
|
|
244
|
+
ablated = logits_with(runner, prompt, mean_ablation(chosen, means), position)
|
|
245
|
+
joint |= {
|
|
246
|
+
"logit_change": float(ablated[token] - base[token]),
|
|
247
|
+
"top_token_changed": int(ablated.argmax()) != token,
|
|
248
|
+
"new_top_token": runner.tokenizer.decode([int(ablated.argmax())]),
|
|
249
|
+
}
|
|
250
|
+
return Explanation(
|
|
251
|
+
prompt, position, runner.tokenizer.decode([token]), logit, poles, total, verified, joint
|
|
252
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Reproducing records made in a 16-bit dtype (bfloat16 or float16), issue #2995.
|
|
2
|
+
|
|
3
|
+
16-bit rounding moves a number by a few percent from one device to another. So a 16-bit
|
|
4
|
+
record is reproduced in float32, and compared within ``reproduce.HALF_TOLERANCE``
|
|
5
|
+
(``docs/analyses/half-precision-reproduce.md``). A pass/fail can then flip where rounding
|
|
6
|
+
moves a number across its threshold. These helpers check that the record's own results
|
|
7
|
+
follow from its own numbers, and its decision from its results.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
HALF = frozenset({"bfloat16", "float16"})
|
|
13
|
+
ROUNDING = 1e-6 # records round activations and deltas to 6 decimals
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def reproduction_dtype(dtype: str) -> str:
|
|
17
|
+
"""The dtype to reproduce a record in: float32 for a 16-bit record, else its own."""
|
|
18
|
+
return "float32" if dtype in HALF else dtype
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def recorded_pass(result, policy, scale: float) -> bool | None:
|
|
22
|
+
"""What the decision rule gives for ``result``'s own recorded number, or None.
|
|
23
|
+
|
|
24
|
+
A probe passes when its activation is at or above the threshold exactly when it is
|
|
25
|
+
expected to be active. A counterfactual passes when its Δ agrees with the expected Δ.
|
|
26
|
+
Other kinds (lookahead) have no single number, so they get None. Records round
|
|
27
|
+
numbers to 6 decimals, so a number within that rounding of its boundary gets None too.
|
|
28
|
+
"""
|
|
29
|
+
from openinterp.agents.review_measurements import _delta_agrees
|
|
30
|
+
|
|
31
|
+
if not result.measured or scale <= 0:
|
|
32
|
+
return None
|
|
33
|
+
if result.kind in {"hard_negative", "positive_holdout"}:
|
|
34
|
+
if result.activation is None:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def rule(x):
|
|
38
|
+
return (x >= policy.activation_threshold * scale) == result.expected_active
|
|
39
|
+
|
|
40
|
+
number = result.activation
|
|
41
|
+
elif result.kind == "counterfactual":
|
|
42
|
+
if result.observed_delta is None or result.expected_delta is None:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def rule(x):
|
|
46
|
+
return _delta_agrees(result.expected_delta, x, policy)
|
|
47
|
+
|
|
48
|
+
number = result.observed_delta
|
|
49
|
+
else:
|
|
50
|
+
return None
|
|
51
|
+
low, high = rule(number - ROUNDING), rule(number + ROUNDING)
|
|
52
|
+
return low if low == high else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def rederived(verdict):
|
|
56
|
+
"""``verdict`` with its metrics and status computed again from its own recorded results."""
|
|
57
|
+
from openinterp.agents.reviewer import decide, review_metrics
|
|
58
|
+
|
|
59
|
+
m = verdict.metrics
|
|
60
|
+
baseline = (
|
|
61
|
+
(m.confound_baseline, m.confound_baseline_accuracy)
|
|
62
|
+
if m.confound_baseline is not None
|
|
63
|
+
else None
|
|
64
|
+
)
|
|
65
|
+
metrics = review_metrics(
|
|
66
|
+
verdict.adversarial_test_results,
|
|
67
|
+
verdict.policy,
|
|
68
|
+
independent=m.independent_review,
|
|
69
|
+
baseline=baseline,
|
|
70
|
+
active_share=m.active_share,
|
|
71
|
+
first_token_floor=m.first_token_floor,
|
|
72
|
+
)
|
|
73
|
+
return verdict.model_copy(
|
|
74
|
+
update={
|
|
75
|
+
"metrics": metrics,
|
|
76
|
+
"status": decide(metrics, verdict.policy),
|
|
77
|
+
"confidence_score": metrics.balanced_accuracy,
|
|
78
|
+
}
|
|
79
|
+
)
|