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
openinterp/reproduce.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Reproduce an experiment record from its JSON before it merges.
|
|
2
|
+
|
|
3
|
+
The command is `open-interpretability reproduce`.
|
|
4
|
+
|
|
5
|
+
`open-interpretability check` only validates a record's shape. This runs again every
|
|
6
|
+
measurement that the verdict rests on. It uses the model revision and dtype that the record pins:
|
|
7
|
+
|
|
8
|
+
1. The scan evidence: each top example's per-token strengths. Also the scale
|
|
9
|
+
(``max_activation``) that every "active" threshold is a fraction of.
|
|
10
|
+
2. Each counterfactual pair's observed Δ.
|
|
11
|
+
3. The review itself, with the record's stored probes and its own ``ReviewPolicy`` (no
|
|
12
|
+
LLM calls). That is every probe's activation and pass/fail, every gate and the status.
|
|
13
|
+
|
|
14
|
+
Given the scan's corpus, it also draws the record's corpus sample again (protocol 0.7),
|
|
15
|
+
from its seed and token budget. It checks that the sample hashes the same and holds
|
|
16
|
+
every top example.
|
|
17
|
+
|
|
18
|
+
Numbers must agree within ``atol × scale + rtol × |recorded|``. Anything the decision
|
|
19
|
+
rule reads (pass/fail, gates, status) must agree exactly. Causal metrics, joint
|
|
20
|
+
ablation and explanation scores are reported but not gated, so a difference there is
|
|
21
|
+
only a warning. Explanation scores are re-derived from re-measured windows and the
|
|
22
|
+
judge's stored answers; the judge and embedding model are not re-run.
|
|
23
|
+
|
|
24
|
+
A record made in a 16-bit dtype (bfloat16 or float16) is reproduced in float32. Its
|
|
25
|
+
numbers are compared within :data:`HALF_TOLERANCE`, because 16-bit rounding moves a
|
|
26
|
+
number by a few percent from one device to another (issue #2995). float32 is also about
|
|
27
|
+
10 times faster than bfloat16 on a CPU. ``half_precision.py`` has the other rules.
|
|
28
|
+
|
|
29
|
+
A record judged under another protocol version can't be reproduced by this harness:
|
|
30
|
+
its decision rule was different. Re-evaluate it instead (`open-interpretability reevaluate`). A
|
|
31
|
+
library upgrade can still shift its activations. So ``measurements_only`` compares its
|
|
32
|
+
scan evidence and counterfactuals, and leaves out the review.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import hashlib
|
|
38
|
+
import math
|
|
39
|
+
from collections.abc import Sequence
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from datetime import date
|
|
42
|
+
from functools import partial
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
from openinterp.core.schemas import PROTOCOL_VERSION, AuditRecord, RunEnvironment
|
|
46
|
+
from openinterp.half_precision import HALF, recorded_pass, rederived, reproduction_dtype
|
|
47
|
+
from openinterp.harness.environment import run_environment
|
|
48
|
+
from openinterp.harness.hooks import NeuronHookRunner
|
|
49
|
+
from openinterp.harness.layer_major import map_layer_major, prompt_tokens
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Tolerance:
|
|
54
|
+
"""Allowed |reproduced − recorded| = atol × scale + rtol × |recorded|.
|
|
55
|
+
|
|
56
|
+
The defaults absorb float32 differences between devices and library versions
|
|
57
|
+
(records round activations to 5–6 decimals) but not an edited number.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
atol: float = 1e-3 # fraction of the neuron's max activation
|
|
61
|
+
rtol: float = 1e-2
|
|
62
|
+
|
|
63
|
+
def close(self, recorded: float | None, reproduced: float | None, scale: float) -> bool:
|
|
64
|
+
"""True when ``reproduced`` is within tolerance of ``recorded``, or both are ``None``."""
|
|
65
|
+
if recorded is None or reproduced is None:
|
|
66
|
+
return recorded is None and reproduced is None
|
|
67
|
+
return abs(reproduced - recorded) <= self.atol * scale + self.rtol * abs(recorded)
|
|
68
|
+
|
|
69
|
+
def for_dtype(self, dtype: str) -> Tolerance:
|
|
70
|
+
"""This tolerance, widened to :data:`HALF_TOLERANCE` for a record in a 16-bit dtype."""
|
|
71
|
+
if dtype not in HALF:
|
|
72
|
+
return self
|
|
73
|
+
return Tolerance(
|
|
74
|
+
atol=max(self.atol, HALF_TOLERANCE.atol), rtol=max(self.rtol, HALF_TOLERANCE.rtol)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
DEFAULT_TOLERANCE = Tolerance()
|
|
79
|
+
# Set from cross-device re-measurements (docs/analyses/half-precision-reproduce.md).
|
|
80
|
+
# At a peak it allows 7%, so a 10% edit of a peak still fails.
|
|
81
|
+
HALF_TOLERANCE = Tolerance(atol=0.05, rtol=0.02)
|
|
82
|
+
# Tokens of one top example that a 16-bit record may have outside HALF_TOLERANCE. Deep
|
|
83
|
+
# layers on unusual text can move a few tokens by up to 22% of the peak; 3 was the most.
|
|
84
|
+
HALF_STRAY_TOKENS = 4
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class ReproductionReport:
|
|
89
|
+
"""The result of reproducing one record: the mismatches, the warnings and what was compared."""
|
|
90
|
+
|
|
91
|
+
path: Path | None
|
|
92
|
+
record: AuditRecord
|
|
93
|
+
mismatches: list[str] = field(default_factory=list)
|
|
94
|
+
warnings: list[str] = field(default_factory=list)
|
|
95
|
+
compared: int = 0
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def ok(self) -> bool:
|
|
99
|
+
"""True when nothing that the decision rule reads differed."""
|
|
100
|
+
return not self.mismatches
|
|
101
|
+
|
|
102
|
+
def fail(self, message: str) -> None:
|
|
103
|
+
"""Record a mismatch."""
|
|
104
|
+
self.mismatches.append(message)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def pinned_environment(record: AuditRecord) -> tuple[str | None, str]:
|
|
108
|
+
"""(model revision, dtype) to load the model with, from the record's own pins."""
|
|
109
|
+
env = record.verdict.environment if record.verdict else None
|
|
110
|
+
prov = record.submission.provenance
|
|
111
|
+
revision = (env and env.model_revision) or (prov and prov.model_revision)
|
|
112
|
+
dtype = (env and env.dtype) or (prov and prov.dtype) or "float32"
|
|
113
|
+
return revision, dtype
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_protocol(record: AuditRecord) -> str | None:
|
|
117
|
+
"""Why this harness can't reproduce ``record``'s verdict, or ``None`` if it can."""
|
|
118
|
+
if record.verdict is None:
|
|
119
|
+
return "record has no verdict"
|
|
120
|
+
policy = record.verdict.policy
|
|
121
|
+
version = policy.protocol_version if policy else "< 0.2"
|
|
122
|
+
if version != PROTOCOL_VERSION:
|
|
123
|
+
return (
|
|
124
|
+
f"judged under protocol {version}, but this harness implements "
|
|
125
|
+
f"{PROTOCOL_VERSION}; reproduce it with that openinterp version, or re-evaluate it"
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def rotating_sample(paths: Sequence[Path], size: int, day: date) -> list[Path]:
|
|
131
|
+
"""Return the ``size`` records of ``paths`` that ``day`` selects.
|
|
132
|
+
|
|
133
|
+
The order comes from a hash of each path, so one day's records are spread over models
|
|
134
|
+
and layers. A new record doesn't move the others in the order. Consecutive days take
|
|
135
|
+
consecutive windows, so a registry of N records is covered in N / ``size`` days.
|
|
136
|
+
"""
|
|
137
|
+
order = sorted(paths, key=lambda p: hashlib.sha256(Path(p).as_posix().encode()).hexdigest())
|
|
138
|
+
if size >= len(order):
|
|
139
|
+
return order
|
|
140
|
+
start = day.toordinal() * size % len(order)
|
|
141
|
+
return (order + order)[start : start + size]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def reproduce_record(
|
|
145
|
+
record: AuditRecord,
|
|
146
|
+
runner: NeuronHookRunner,
|
|
147
|
+
tolerance: Tolerance = DEFAULT_TOLERANCE,
|
|
148
|
+
path: Path | None = None,
|
|
149
|
+
corpus: tuple[list[str], str] | None = None,
|
|
150
|
+
*,
|
|
151
|
+
measurements_only: bool = False,
|
|
152
|
+
) -> ReproductionReport:
|
|
153
|
+
"""Reproduce ``record`` with ``runner``.
|
|
154
|
+
|
|
155
|
+
``corpus`` is (documents, SHA-256 of the corpus file), to draw the scan's sample from.
|
|
156
|
+
With ``measurements_only``, a record from another protocol version gets its scan
|
|
157
|
+
evidence and counterfactuals compared, and a warning that the review was left out.
|
|
158
|
+
"""
|
|
159
|
+
report = ReproductionReport(path=path, record=record)
|
|
160
|
+
reason = check_protocol(record)
|
|
161
|
+
if reason and (record.verdict is None or not measurements_only):
|
|
162
|
+
report.fail(reason)
|
|
163
|
+
return report
|
|
164
|
+
from openinterp.harness.units import submission_unit
|
|
165
|
+
|
|
166
|
+
sub, verdict = record.submission, record.verdict
|
|
167
|
+
unit = submission_unit(sub)
|
|
168
|
+
half = pinned_environment(record)[1] in HALF
|
|
169
|
+
tolerance = tolerance.for_dtype(pinned_environment(record)[1])
|
|
170
|
+
_compare_environment(record, run_environment(runner), report)
|
|
171
|
+
|
|
172
|
+
scale = sub.max_activation or 0.0
|
|
173
|
+
_check_scale(sub, unit, runner, scale, report)
|
|
174
|
+
|
|
175
|
+
# 1. Scan evidence.
|
|
176
|
+
if corpus is not None:
|
|
177
|
+
_check_sample(sub.provenance, sub.top_examples, runner, *corpus, report)
|
|
178
|
+
stray = HALF_STRAY_TOKENS if half else 0
|
|
179
|
+
_check_examples(sub.top_examples, unit, runner, tolerance, scale, report, stray)
|
|
180
|
+
|
|
181
|
+
# 2. Counterfactuals as submitted.
|
|
182
|
+
_check_counterfactuals(sub, unit, runner, tolerance, scale, report)
|
|
183
|
+
if reason:
|
|
184
|
+
report.warnings.append(f"measurements only, because the record was {reason}")
|
|
185
|
+
return report
|
|
186
|
+
|
|
187
|
+
# 3. The review, under the record's own policy and reviewer.
|
|
188
|
+
_compare_verdicts(verdict, _review_again(record, runner), tolerance, scale, report, half=half)
|
|
189
|
+
|
|
190
|
+
# 4. Explanation scores (reported, not gated): re-measure the windows, keep the answers.
|
|
191
|
+
if verdict.explanation_scores is not None:
|
|
192
|
+
_compare_explanation_scores(
|
|
193
|
+
verdict.explanation_scores, runner, unit, tolerance, scale, report
|
|
194
|
+
)
|
|
195
|
+
return report
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _review_again(record: AuditRecord, runner):
|
|
199
|
+
"""The record's probes and pairs measured again, under its own policy and reviewer."""
|
|
200
|
+
from openinterp.agents.reviewer import evaluate_submission, probes_from_verdict
|
|
201
|
+
|
|
202
|
+
verdict = record.verdict
|
|
203
|
+
return evaluate_submission(
|
|
204
|
+
record.submission,
|
|
205
|
+
runner,
|
|
206
|
+
probes_from_verdict(verdict),
|
|
207
|
+
verdict.reviewer_agent_model,
|
|
208
|
+
verdict.policy,
|
|
209
|
+
wire=False, # reported, not gated: nothing to compare
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _check_scale(sub, unit, runner, scale: float, report) -> None:
|
|
214
|
+
"""``max_activation`` is the top example's peak, and the first-token floor reproduces."""
|
|
215
|
+
peak = max((e.max_activation for e in sub.top_examples), default=None)
|
|
216
|
+
if peak is not None and not math.isclose(scale, peak, rel_tol=1e-9, abs_tol=1e-12):
|
|
217
|
+
report.fail(f"max_activation {scale} is not the top example's peak {peak}")
|
|
218
|
+
if sub.first_token_floor is not None: # judged against the scan's scale (protocol 0.8)
|
|
219
|
+
from functools import partial
|
|
220
|
+
|
|
221
|
+
from openinterp.core.schemas import ReviewPolicy
|
|
222
|
+
from openinterp.harness.scan import has_first_token_floor
|
|
223
|
+
|
|
224
|
+
read = partial(unit.strengths, runner)
|
|
225
|
+
if has_first_token_floor(runner, read, scale, ReviewPolicy()) != sub.first_token_floor:
|
|
226
|
+
report.fail(f"first_token_floor {sub.first_token_floor} does not reproduce")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _check_counterfactuals(sub, unit, runner, tolerance: Tolerance, scale: float, report):
|
|
230
|
+
from openinterp.agents.proposer import observed_delta
|
|
231
|
+
|
|
232
|
+
pairs = [(n, p) for n, p in enumerate(sub.counterfactuals, 1) if p.observed_delta is not None]
|
|
233
|
+
if not scale:
|
|
234
|
+
return
|
|
235
|
+
texts = [t for _, p in pairs for t in (p.base_prompt, p.counterfactual_prompt)]
|
|
236
|
+
deltas = map_layer_major(
|
|
237
|
+
runner, lambda item: observed_delta(runner, unit, item[1], scale), pairs,
|
|
238
|
+
tokens=prompt_tokens(runner, texts), logits=False,
|
|
239
|
+
) # fmt: skip
|
|
240
|
+
for (n, pair), delta in zip(pairs, deltas, strict=True):
|
|
241
|
+
report.compared += 1
|
|
242
|
+
if not tolerance.close(pair.observed_delta, delta, 1.0):
|
|
243
|
+
report.fail(
|
|
244
|
+
f"counterfactual {n}: observed_delta recorded {pair.observed_delta}, "
|
|
245
|
+
f"reproduced {delta}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _check_examples(
|
|
250
|
+
examples, unit, runner, tolerance: Tolerance, scale: float, report, stray: int = 0
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Each top example's tokens and activations, measured layer-major when streamed.
|
|
253
|
+
|
|
254
|
+
Up to ``stray`` tokens of each example may differ beyond ``tolerance``, as a warning.
|
|
255
|
+
"""
|
|
256
|
+
texts = [ex.text for ex in examples]
|
|
257
|
+
measured = map_layer_major(
|
|
258
|
+
runner, partial(unit.strengths, runner), texts,
|
|
259
|
+
tokens=prompt_tokens(runner, texts), logits=False,
|
|
260
|
+
) # fmt: skip
|
|
261
|
+
for n, (ex, strengths) in enumerate(zip(examples, measured, strict=True), 1):
|
|
262
|
+
_check_example(n, ex, unit, runner, tolerance, scale, report, strengths, stray)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _check_example(
|
|
266
|
+
n: int, ex, unit, runner, tolerance: Tolerance, scale: float, report, strengths, stray=0
|
|
267
|
+
) -> None:
|
|
268
|
+
"""Top example ``n``'s tokens and per-token activations (``strengths``), and its peak."""
|
|
269
|
+
tokens = runner.encode(ex.text)["input_ids"][0].tolist()
|
|
270
|
+
words = [runner.tokenizer.decode([i]) for i in tokens] if runner.tokenizer else None
|
|
271
|
+
if words is not None and words != ex.tokens:
|
|
272
|
+
report.fail(f"top example {n}: tokens differ from the record")
|
|
273
|
+
return
|
|
274
|
+
values = strengths[0].tolist()
|
|
275
|
+
report.compared += len(values)
|
|
276
|
+
bad = [
|
|
277
|
+
i
|
|
278
|
+
for i, (a, b) in enumerate(zip(ex.activations, values, strict=False))
|
|
279
|
+
if not tolerance.close(a, b, scale)
|
|
280
|
+
]
|
|
281
|
+
if bad and len(bad) <= stray and len(values) == len(ex.activations):
|
|
282
|
+
report.warnings.append(
|
|
283
|
+
f"top example {n}: {len(bad)} token(s) differ beyond the 16-bit tolerance, "
|
|
284
|
+
f"e.g. token {bad[0]} (recorded {ex.activations[bad[0]]}, "
|
|
285
|
+
f"reproduced {round(values[bad[0]], 6)})"
|
|
286
|
+
)
|
|
287
|
+
bad = []
|
|
288
|
+
if len(values) != len(ex.activations) or bad:
|
|
289
|
+
where = f"token {bad[0]}" if bad else "length"
|
|
290
|
+
report.fail(
|
|
291
|
+
f"top example {n}: activations differ at {where} "
|
|
292
|
+
f"(recorded {ex.activations[bad[0]] if bad else len(ex.activations)}, "
|
|
293
|
+
f"reproduced {round(values[bad[0]], 6) if bad else len(values)})"
|
|
294
|
+
)
|
|
295
|
+
measured = values[int(unit.skip_first) :]
|
|
296
|
+
if not tolerance.close(ex.max_activation, max(measured, default=0.0), scale):
|
|
297
|
+
report.fail(f"top example {n}: max_activation differs")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _check_sample(prov, examples, runner, texts, corpus_sha, report) -> None:
|
|
301
|
+
"""Re-draw the record's corpus sample: the same documents, holding every top example."""
|
|
302
|
+
from openinterp.corpus import sample_tokens
|
|
303
|
+
|
|
304
|
+
sample = prov and prov.corpus_sample
|
|
305
|
+
if sample is None:
|
|
306
|
+
return
|
|
307
|
+
if corpus_sha != prov.corpus_sha256:
|
|
308
|
+
report.warnings.append("the corpus given is not the scan's; its sample was not checked")
|
|
309
|
+
return
|
|
310
|
+
drawn, redrawn = sample_tokens(runner.tokenizer, texts, sample.max_tokens, sample.seed)
|
|
311
|
+
if redrawn != sample:
|
|
312
|
+
report.fail(f"corpus sample recorded {sample}, re-drawn {redrawn}")
|
|
313
|
+
return
|
|
314
|
+
for n, ex in enumerate(examples, 1):
|
|
315
|
+
if not any(ex.text in doc for doc in drawn):
|
|
316
|
+
report.fail(f"top example {n} is not in the corpus sample")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _compare_explanation_scores(
|
|
320
|
+
old, runner, unit, tolerance: Tolerance, scale: float, report
|
|
321
|
+
) -> None:
|
|
322
|
+
from openinterp.harness.explanation_scores import remeasure
|
|
323
|
+
|
|
324
|
+
new = remeasure(old, runner, unit)
|
|
325
|
+
for kind in ("detection", "fuzzing"):
|
|
326
|
+
pairs = zip(getattr(old, f"{kind}_windows"), getattr(new, f"{kind}_windows"), strict=True)
|
|
327
|
+
for n, (a, b) in enumerate(pairs, 1):
|
|
328
|
+
report.compared += 1
|
|
329
|
+
same = len(a.activations) == len(b.activations) and all(
|
|
330
|
+
tolerance.close(x, y, scale)
|
|
331
|
+
for x, y in zip(a.activations, b.activations, strict=True)
|
|
332
|
+
)
|
|
333
|
+
if not same or a.label != b.label:
|
|
334
|
+
report.warnings.append(
|
|
335
|
+
f"explanation scores: {kind} window {n} re-measures differently "
|
|
336
|
+
f"(label {a.label} -> {b.label})"
|
|
337
|
+
)
|
|
338
|
+
for name in ("detection", "fuzzing", "embedding_auroc"):
|
|
339
|
+
if getattr(old, name) != getattr(new, name):
|
|
340
|
+
report.warnings.append(f"explanation scores: {name} differs (recorded, not gated)")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _compare_environment(record: AuditRecord, now: RunEnvironment, report) -> None:
|
|
344
|
+
revision, dtype = pinned_environment(record)
|
|
345
|
+
if revision and now.model_revision and revision != now.model_revision:
|
|
346
|
+
report.fail(f"model revision {now.model_revision} is not the pinned {revision}")
|
|
347
|
+
if dtype not in {"auto", now.dtype} and reproduction_dtype(dtype) != now.dtype:
|
|
348
|
+
report.fail(f"loaded {now.dtype} weights; the record pins {dtype}")
|
|
349
|
+
elif dtype in HALF and now.dtype != dtype:
|
|
350
|
+
report.warnings.append(
|
|
351
|
+
f"reproduced in {now.dtype}; the record pins {dtype}, so its numbers are compared "
|
|
352
|
+
"within the 16-bit tolerance"
|
|
353
|
+
)
|
|
354
|
+
then = record.verdict.environment
|
|
355
|
+
if then is None:
|
|
356
|
+
report.warnings.append("record predates environment pinning; device and versions unknown")
|
|
357
|
+
return
|
|
358
|
+
names = ("device", "device_name", "torch_version", "transformers_version")
|
|
359
|
+
for name in (*names, "experts_implementation"):
|
|
360
|
+
if name == "device_name" and then.device_name is None: # recorded before 0.0.19
|
|
361
|
+
continue
|
|
362
|
+
if getattr(then, name) != getattr(now, name):
|
|
363
|
+
report.warnings.append(
|
|
364
|
+
f"{name}: recorded {getattr(then, name)}, now {getattr(now, name)}"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _compare_verdicts(
|
|
369
|
+
old, new, tolerance: Tolerance, scale: float, report, *, half: bool = False
|
|
370
|
+
) -> None:
|
|
371
|
+
"""Each test's numbers and result, then the decision.
|
|
372
|
+
|
|
373
|
+
For a 16-bit record (``half``), a result may flip where 16-bit rounding moves a number
|
|
374
|
+
across its threshold. The flip is only a warning when the numbers agree within
|
|
375
|
+
``tolerance`` and the recorded result follows from the recorded number. The recorded
|
|
376
|
+
decision must then follow exactly from the recorded results.
|
|
377
|
+
"""
|
|
378
|
+
if len(old.adversarial_test_results) != len(new.adversarial_test_results):
|
|
379
|
+
report.fail("the record's test results don't match the probes and pairs it stores")
|
|
380
|
+
return
|
|
381
|
+
flips = 0
|
|
382
|
+
for n, (a, b) in enumerate(
|
|
383
|
+
zip(old.adversarial_test_results, new.adversarial_test_results, strict=True), 1
|
|
384
|
+
):
|
|
385
|
+
flips += _compare_result(n, a, b, old.policy, tolerance, scale, report, half)
|
|
386
|
+
|
|
387
|
+
if flips and old.metrics is None:
|
|
388
|
+
report.fail("a result flipped, and the record has no metrics to re-derive")
|
|
389
|
+
elif flips:
|
|
390
|
+
_compare_decision(old, rederived(old), report)
|
|
391
|
+
if (old.metrics and old.metrics.gates) != new.metrics.gates or old.status != new.status:
|
|
392
|
+
report.warnings.append(
|
|
393
|
+
f"reproduced decision {new.status} with gates {new.metrics.gates}; the "
|
|
394
|
+
"recorded decision follows from the recorded results"
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
_compare_decision(old, new, report)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _compare_result(n, a, b, policy, tolerance, scale: float, report, half: bool) -> int:
|
|
401
|
+
"""Compare recorded test ``a`` with reproduced test ``b``. Return 1 for a 16-bit flip."""
|
|
402
|
+
label = f"test {n} ({a.kind}: {a.prompt[:40]!r})"
|
|
403
|
+
if (a.kind, a.prompt) != (b.kind, b.prompt):
|
|
404
|
+
report.fail(f"{label}: does not match the stored probe")
|
|
405
|
+
return 0
|
|
406
|
+
report.compared += 1
|
|
407
|
+
close = tolerance.close(a.activation, b.activation, scale)
|
|
408
|
+
if not close:
|
|
409
|
+
report.fail(f"{label}: activation recorded {a.activation}, reproduced {b.activation}")
|
|
410
|
+
if not tolerance.close(a.observed_delta, b.observed_delta, 1.0):
|
|
411
|
+
close = False
|
|
412
|
+
report.fail(f"{label}: Δ recorded {a.observed_delta}, reproduced {b.observed_delta}")
|
|
413
|
+
follows = recorded_pass(a, policy, scale)
|
|
414
|
+
if half and follows not in {None, a.passed}:
|
|
415
|
+
report.fail(f"{label}: recorded passed={a.passed} does not follow from its number")
|
|
416
|
+
elif a.passed != b.passed:
|
|
417
|
+
if half and close and follows is not None:
|
|
418
|
+
report.warnings.append(
|
|
419
|
+
f"{label}: passed={a.passed} recorded, {b.passed} reproduced, at its "
|
|
420
|
+
"threshold within 16-bit rounding"
|
|
421
|
+
)
|
|
422
|
+
return 1
|
|
423
|
+
report.fail(f"{label}: recorded passed={a.passed}, reproduced passed={b.passed}")
|
|
424
|
+
return 0
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _compare_decision(old, new, report) -> None:
|
|
428
|
+
"""Gates, balanced accuracy and status must match; causal metrics only warn."""
|
|
429
|
+
if old.metrics is None or old.metrics.gates != new.metrics.gates:
|
|
430
|
+
report.fail(
|
|
431
|
+
f"gates recorded {old.metrics.gates if old.metrics else None}, "
|
|
432
|
+
f"reproduced {new.metrics.gates}"
|
|
433
|
+
)
|
|
434
|
+
if not math.isclose(old.confidence_score, new.confidence_score, abs_tol=1e-6):
|
|
435
|
+
report.fail(
|
|
436
|
+
f"balanced accuracy recorded {old.confidence_score}, reproduced {new.confidence_score}"
|
|
437
|
+
)
|
|
438
|
+
if old.status != new.status:
|
|
439
|
+
report.fail(f"status recorded {old.status}, reproduced {new.status}")
|
|
440
|
+
|
|
441
|
+
oc, nc = old.causal_metrics, new.causal_metrics
|
|
442
|
+
if (oc is None) != (nc is None) or (
|
|
443
|
+
oc
|
|
444
|
+
and nc
|
|
445
|
+
and (
|
|
446
|
+
abs(oc.sufficiency_score - nc.sufficiency_score) > 0.1
|
|
447
|
+
or abs(oc.necessity_score - nc.necessity_score) > 0.1
|
|
448
|
+
)
|
|
449
|
+
):
|
|
450
|
+
report.warnings.append("causal metrics differ (recorded, not gated)")
|
openinterp/revision.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""What a revision's proposer sees of the record that it supersedes (issue #3246).
|
|
2
|
+
|
|
3
|
+
``open-interpretability audit --supersedes <record>`` revises an earlier hypothesis about the same
|
|
4
|
+
unit. Without this, the proposer wrote its new hypothesis from the scan alone and could
|
|
5
|
+
repeat the old mistake. The brief gives the old hypothesis, its verdict, the gates it
|
|
6
|
+
failed and the probes it got wrong. The proposer may see the measurements: only the
|
|
7
|
+
reviewer must not.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from openinterp.core.fence import fence
|
|
13
|
+
|
|
14
|
+
MAX_WRONG = 12 # wrong probes listed, so a long probe set doesn't flood the prompt
|
|
15
|
+
PROBES = ("hard_negative", "positive_holdout")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def previous_attempt(record) -> str:
|
|
19
|
+
"""Return the brief on ``record`` for the proposer of the hypothesis that supersedes it."""
|
|
20
|
+
verdict = record.verdict
|
|
21
|
+
gates = verdict.metrics.gates if verdict.metrics else {}
|
|
22
|
+
failed = [gate for gate, ok in gates.items() if not ok]
|
|
23
|
+
wrong = [
|
|
24
|
+
r
|
|
25
|
+
for r in verdict.adversarial_test_results
|
|
26
|
+
if r.kind in PROBES and r.measured and not r.passed
|
|
27
|
+
]
|
|
28
|
+
lines = [
|
|
29
|
+
"This hypothesis revises an earlier one about this unit. Fix what failed, and "
|
|
30
|
+
"keep what the earlier review confirmed.",
|
|
31
|
+
f"Earlier hypothesis:\n{fence(record.submission.natural_language_explanation)}",
|
|
32
|
+
f"Its verdict: {verdict.status}. Failed gates: {', '.join(failed) or 'none'}.",
|
|
33
|
+
]
|
|
34
|
+
if wrong:
|
|
35
|
+
lines.append(f"Probes it got wrong ({min(len(wrong), MAX_WRONG)} of {len(wrong)}):")
|
|
36
|
+
lines += [_probe_line(r) for r in wrong[:MAX_WRONG]]
|
|
37
|
+
return "\n".join(lines) + "\n"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _probe_line(result) -> str:
|
|
41
|
+
"""One wrong probe: what the hypothesis predicted, and what was measured."""
|
|
42
|
+
predicted = "active" if result.expected_active else "inactive"
|
|
43
|
+
measured = "n/a" if result.activation is None else f"{result.activation:.2f}"
|
|
44
|
+
kind = result.kind.replace("_", " ")
|
|
45
|
+
return f"- {kind}, predicted {predicted}, activation {measured}: {result.prompt!r}"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Keep loaded models across commands that run in one process (``open-interpretability loop``).
|
|
2
|
+
|
|
3
|
+
Each ``open-interpretability`` command loads its model afresh, which is right for one command
|
|
4
|
+
per process. ``open-interpretability loop`` runs many commands in one process, so it turns this
|
|
5
|
+
cache on. Then each experiment on a model after the first skips the load. The cache is off by
|
|
6
|
+
default.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
|
|
13
|
+
_runners: dict[tuple, object] | None = None # None: the cache is off
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def keep() -> None:
|
|
17
|
+
"""Turn the cache on for the rest of this process."""
|
|
18
|
+
global _runners
|
|
19
|
+
if _runners is None:
|
|
20
|
+
_runners = {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def cached(load):
|
|
24
|
+
"""Wrap ``load`` so that, while the cache is on, equal arguments return one runner."""
|
|
25
|
+
|
|
26
|
+
@functools.wraps(load)
|
|
27
|
+
def wrapper(*args, **kwargs):
|
|
28
|
+
if _runners is None:
|
|
29
|
+
return load(*args, **kwargs)
|
|
30
|
+
key = (args, tuple(sorted(kwargs.items())))
|
|
31
|
+
if key not in _runners:
|
|
32
|
+
_runners[key] = load(*args, **kwargs)
|
|
33
|
+
return _runners[key]
|
|
34
|
+
|
|
35
|
+
return wrapper
|