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,462 @@
|
|
|
1
|
+
"""One pass over a corpus that gathers everything an audit needs about a neuron pole.
|
|
2
|
+
|
|
3
|
+
For every non-padding token, it records three things:
|
|
4
|
+
|
|
5
|
+
- the pole strength, for the top examples and histogram;
|
|
6
|
+
- the whole layer's activations, for partner neuron poles (co-activation lift, Spearman,
|
|
7
|
+
Pearson);
|
|
8
|
+
- the token id and position, for the confound profile.
|
|
9
|
+
|
|
10
|
+
Documents are scanned whole by default. With ``window_tokens``, each is cut into
|
|
11
|
+
overlapping windows that start every ``stride`` tokens. So a long document is read in
|
|
12
|
+
full without one huge forward pass. Every token still counts once in the statistics. It
|
|
13
|
+
counts in the first window after the first that reaches it. There it has at least
|
|
14
|
+
``window_tokens - stride`` tokens of context (the strided-perplexity convention).
|
|
15
|
+
A unit with a first-token floor (protocol 0.8) is above its line at the first token,
|
|
16
|
+
whatever the token. So its scan leaves first tokens out of its scale, histogram and
|
|
17
|
+
top-example ranking (:func:`settle`). That is decided after the pass, so every scan
|
|
18
|
+
keeps whether each counted token is its piece's first.
|
|
19
|
+
Top examples are windows, decoded back to text so a record re-measures them as the
|
|
20
|
+
scan did, and no two overlap.
|
|
21
|
+
|
|
22
|
+
Each forward pass yields a :class:`Chunk`: the whole layer's activations on every real
|
|
23
|
+
token of its pieces. Given a :class:`~openinterp.harness.layer_cache.LayerCache`, a scan
|
|
24
|
+
stores its chunks, and a later scan of any neuron in that layer replays them.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
import torch
|
|
34
|
+
|
|
35
|
+
from openinterp.core.schemas import (
|
|
36
|
+
ActivatingExample,
|
|
37
|
+
ConfoundProfile,
|
|
38
|
+
CorrelatedNeuron,
|
|
39
|
+
Pole,
|
|
40
|
+
ReviewPolicy,
|
|
41
|
+
)
|
|
42
|
+
from openinterp.harness.annotations import LabelPass, Tagger, TokenLabels
|
|
43
|
+
from openinterp.harness.confounds import ConfoundAccumulator
|
|
44
|
+
from openinterp.harness.covariance import PartnerSearch
|
|
45
|
+
from openinterp.harness.hooks import Inputs, NeuronHookRunner, pole_strength
|
|
46
|
+
from openinterp.harness.scan_backend import forward_chunks, rescore_scan, with_ane_fallback
|
|
47
|
+
|
|
48
|
+
if TYPE_CHECKING:
|
|
49
|
+
from openinterp.harness.layer_cache import LayerCache
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Padded tokens per forward pass (pieces × the longest). Part of a cached scan's key, as
|
|
53
|
+
# it sets padding. On Qwen2.5-0.5B over corpus.jsonl, the model reads 27% fewer tokens
|
|
54
|
+
# (padding included) than with 8 pieces a pass. 2048 is near the fastest budget on
|
|
55
|
+
# Apple's GPU.
|
|
56
|
+
BATCH_TOKENS = 2048
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ScanResult:
|
|
61
|
+
"""What one scan found: top examples, every counted strength, partners and confounds."""
|
|
62
|
+
|
|
63
|
+
examples: list[ActivatingExample]
|
|
64
|
+
values: list[float] # every scanned token's pole strength, each token once
|
|
65
|
+
correlated: list[CorrelatedNeuron]
|
|
66
|
+
confounds: ConfoundProfile
|
|
67
|
+
windows: int = 0 # forward-pass inputs: documents, or windows of them
|
|
68
|
+
opposite: OppositeScan | None = None # the neuron's other pole, from the same pass
|
|
69
|
+
# Protocol 0.8: first tokens are left out of examples and values (see ``settle``).
|
|
70
|
+
first_token_floor: bool = False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class OppositeScan:
|
|
75
|
+
"""The neuron's other pole, from the same pass: its top examples and strengths."""
|
|
76
|
+
|
|
77
|
+
pole: Pole
|
|
78
|
+
examples: list[ActivatingExample]
|
|
79
|
+
values: list[float]
|
|
80
|
+
first_token_floor: bool = False
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
Strengths = Callable[[Inputs], torch.Tensor]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@torch.no_grad()
|
|
87
|
+
def first_position_share(
|
|
88
|
+
runner: NeuronHookRunner, strengths: Strengths, line: float, batch_size: int = 8192
|
|
89
|
+
) -> float:
|
|
90
|
+
"""Return the share of the vocabulary on which ``strengths`` is at or above ``line``.
|
|
91
|
+
|
|
92
|
+
This is at a text's first position, with special tokens left out. Attention there
|
|
93
|
+
sees only the token itself. So this is a property of the weights, the same for every
|
|
94
|
+
corpus.
|
|
95
|
+
"""
|
|
96
|
+
from openinterp.harness import first_position
|
|
97
|
+
|
|
98
|
+
if reader := getattr(strengths, "first_position", None): # a neuron's: computed once
|
|
99
|
+
values = reader()
|
|
100
|
+
else:
|
|
101
|
+
values = first_position.strengths_of(runner, strengths, batch_size)
|
|
102
|
+
return float((values >= line).sum()) / max(len(values), 1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def has_first_token_floor(
|
|
106
|
+
runner: NeuronHookRunner, strengths: Strengths, scale: float, policy: ReviewPolicy
|
|
107
|
+
) -> bool:
|
|
108
|
+
"""Return whether the unit has a first-token floor (protocol 0.8).
|
|
109
|
+
|
|
110
|
+
It has one when it is active at the first position on at least
|
|
111
|
+
``policy.max_active_share`` of the vocabulary. Its line comes from ``scale``.
|
|
112
|
+
"""
|
|
113
|
+
if scale <= 0:
|
|
114
|
+
return False
|
|
115
|
+
line = policy.activation_threshold * scale
|
|
116
|
+
return first_position_share(runner, strengths, line) >= policy.max_active_share
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def settle(
|
|
120
|
+
runner: NeuronHookRunner,
|
|
121
|
+
strengths: Strengths,
|
|
122
|
+
values: Sequence[float],
|
|
123
|
+
first: Sequence[bool],
|
|
124
|
+
top: TopExamples,
|
|
125
|
+
k: int,
|
|
126
|
+
labels: TokenLabels | None = None,
|
|
127
|
+
) -> tuple[list[ActivatingExample], list[float], bool]:
|
|
128
|
+
"""Return a scan's top examples, counted values, and whether it has a first-token floor.
|
|
129
|
+
|
|
130
|
+
The floor is judged against the scale that the unit would have without first tokens.
|
|
131
|
+
That is the top example's peak after its first token. It is the scale that the unit
|
|
132
|
+
records if it has the floor. With the floor, first tokens are left out of the
|
|
133
|
+
examples' ranking and of the values. Each example carries its peak token's ``labels``.
|
|
134
|
+
"""
|
|
135
|
+
best = top.top(runner.tokenizer, 1, skip_first=True)
|
|
136
|
+
scale = best[0].max_activation if best else 0.0
|
|
137
|
+
floor = has_first_token_floor(runner, strengths, scale, ReviewPolicy())
|
|
138
|
+
kept = [v for v, f in zip(values, first, strict=True) if not f] if floor else list(values)
|
|
139
|
+
return top.top(runner.tokenizer, k, skip_first=floor, labels=labels), kept, floor
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class Piece:
|
|
144
|
+
"""What one forward pass reads: a whole document, or tokens [start, end) of it."""
|
|
145
|
+
|
|
146
|
+
doc: int
|
|
147
|
+
start: int
|
|
148
|
+
end: int
|
|
149
|
+
text: str
|
|
150
|
+
fresh: int | None = None # trailing tokens new to the scan; None: every token is
|
|
151
|
+
|
|
152
|
+
def overlaps(self, other: Piece) -> bool:
|
|
153
|
+
"""True when the two pieces share tokens of one document."""
|
|
154
|
+
return self.doc == other.doc and self.start < other.end and other.start < self.end
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def default_stride(window_tokens: int) -> int:
|
|
158
|
+
"""Half a window, so every counted token has at least half a window of context."""
|
|
159
|
+
return max(window_tokens // 2, 1)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cut_pieces(
|
|
163
|
+
tokenizer, texts: Sequence[str], window_tokens: int | None, stride: int | None
|
|
164
|
+
) -> Iterator[Piece]:
|
|
165
|
+
"""Whole documents, or each document's windows (see the module docstring)."""
|
|
166
|
+
if window_tokens is None:
|
|
167
|
+
yield from (Piece(doc, 0, 1, text) for doc, text in enumerate(texts))
|
|
168
|
+
return
|
|
169
|
+
stride = stride or default_stride(window_tokens)
|
|
170
|
+
if not 0 < stride <= window_tokens:
|
|
171
|
+
raise ValueError(f"stride must be in 1..{window_tokens}, got {stride}")
|
|
172
|
+
for doc, text in enumerate(texts):
|
|
173
|
+
ids = tokenizer.encode(text, add_special_tokens=False)
|
|
174
|
+
start = prev_end = 0
|
|
175
|
+
while start < len(ids):
|
|
176
|
+
end = min(start + window_tokens, len(ids))
|
|
177
|
+
fresh = None if start == 0 else end - prev_end
|
|
178
|
+
yield Piece(doc, start, end, tokenizer.decode(ids[start:end]), fresh)
|
|
179
|
+
if end == len(ids):
|
|
180
|
+
break
|
|
181
|
+
start, prev_end = start + stride, end
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class Batch:
|
|
186
|
+
"""One forward pass of pieces, with which of its tokens the scan counts."""
|
|
187
|
+
|
|
188
|
+
pieces: list[Piece]
|
|
189
|
+
encoded: dict[str, torch.Tensor]
|
|
190
|
+
ids: torch.Tensor # [b, seq], on the CPU
|
|
191
|
+
mask: torch.Tensor # real (non-padding) tokens
|
|
192
|
+
counted: torch.Tensor # real tokens new to the scan
|
|
193
|
+
positions: torch.Tensor
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def first(self) -> torch.Tensor:
|
|
197
|
+
"""Each piece's first real token."""
|
|
198
|
+
return self.mask & (self.mask.cumsum(1) == 1)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def plan_batches(lengths: Sequence[int], batch_tokens: int) -> list[int]:
|
|
202
|
+
"""Return how many consecutive pieces each forward pass takes.
|
|
203
|
+
|
|
204
|
+
That is as many as fit in ``batch_tokens`` once padded to the longest, and at least one.
|
|
205
|
+
"""
|
|
206
|
+
sizes: list[int] = []
|
|
207
|
+
size = longest = 0
|
|
208
|
+
for n in lengths:
|
|
209
|
+
if size and max(longest, n) * (size + 1) > batch_tokens:
|
|
210
|
+
sizes.append(size)
|
|
211
|
+
size = longest = 0
|
|
212
|
+
size, longest = size + 1, max(longest, n)
|
|
213
|
+
return [*sizes, size] if size else sizes
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def batches(
|
|
217
|
+
runner: NeuronHookRunner, pieces: Sequence[Piece], batch_tokens: int = BATCH_TOKENS
|
|
218
|
+
) -> Iterator[Batch]:
|
|
219
|
+
"""``pieces`` in order, in forward passes of at most ``batch_tokens`` padded tokens."""
|
|
220
|
+
if not pieces:
|
|
221
|
+
return
|
|
222
|
+
every = runner.tokenizer([p.text for p in pieces], return_tensors="pt", padding=True)
|
|
223
|
+
lengths = every.get("attention_mask", torch.ones_like(every["input_ids"])).sum(1).tolist()
|
|
224
|
+
start = 0
|
|
225
|
+
for size in plan_batches(lengths, batch_tokens):
|
|
226
|
+
batch = list(pieces[start : start + size])
|
|
227
|
+
start += size
|
|
228
|
+
encoded = runner.encode([p.text for p in batch])
|
|
229
|
+
ids = encoded["input_ids"].cpu()
|
|
230
|
+
mask = encoded.get("attention_mask", torch.ones_like(encoded["input_ids"])).bool().cpu()
|
|
231
|
+
# Count only each piece's fresh tail: real tokens at or after it ≤ fresh.
|
|
232
|
+
fresh = torch.tensor([p.fresh or ids.shape[1] for p in batch]).unsqueeze(1)
|
|
233
|
+
counted = mask & (mask.flip(1).cumsum(1).flip(1) <= fresh)
|
|
234
|
+
positions = torch.arange(ids.shape[1]).expand_as(ids)
|
|
235
|
+
yield Batch(batch, encoded, ids, mask, counted, positions)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass
|
|
239
|
+
class Chunk:
|
|
240
|
+
"""One forward pass, flattened to its pieces' real tokens in scan order."""
|
|
241
|
+
|
|
242
|
+
pieces: list[Piece]
|
|
243
|
+
lengths: list[int] # real tokens of each piece
|
|
244
|
+
ids: torch.Tensor # [n]
|
|
245
|
+
positions: torch.Tensor # [n]
|
|
246
|
+
counted: torch.Tensor # [n] bool: tokens new to the scan
|
|
247
|
+
acts: torch.Tensor # [n, width] the whole layer, in the model's dtype, on the CPU
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def first(self) -> torch.Tensor:
|
|
251
|
+
"""``[n]`` bool: each piece's first token (pieces are padded on the right)."""
|
|
252
|
+
return self.positions == 0
|
|
253
|
+
|
|
254
|
+
def rows(self) -> Iterator[tuple[Piece, slice]]:
|
|
255
|
+
"""Each piece and its rows."""
|
|
256
|
+
start = 0
|
|
257
|
+
for piece, n in zip(self.pieces, self.lengths, strict=True):
|
|
258
|
+
yield piece, slice(start, start + n)
|
|
259
|
+
start += n
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def layer_chunks(
|
|
263
|
+
runner: NeuronHookRunner, pieces: Sequence[Piece], layer: int, batch_tokens: int
|
|
264
|
+
) -> Iterator[Chunk]:
|
|
265
|
+
"""Run the model over ``pieces`` and yield ``layer``'s activations batch by batch."""
|
|
266
|
+
for batch in batches(runner, pieces, batch_tokens):
|
|
267
|
+
cache = runner.layer_activations(batch.encoded, [layer])
|
|
268
|
+
yield Chunk(
|
|
269
|
+
pieces=batch.pieces,
|
|
270
|
+
lengths=batch.mask.sum(1).tolist(),
|
|
271
|
+
ids=batch.ids[batch.mask],
|
|
272
|
+
positions=batch.positions[batch.mask],
|
|
273
|
+
counted=batch.counted[batch.mask],
|
|
274
|
+
acts=cache[layer].cpu()[batch.mask],
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def scan_chunks(
|
|
279
|
+
runner: NeuronHookRunner,
|
|
280
|
+
pieces: Sequence[Piece],
|
|
281
|
+
layer: int,
|
|
282
|
+
batch_tokens: int = BATCH_TOKENS,
|
|
283
|
+
cache: LayerCache | None = None,
|
|
284
|
+
) -> Iterator[Chunk]:
|
|
285
|
+
"""Yield ``layer``'s chunks, replayed from ``cache`` if it holds them.
|
|
286
|
+
|
|
287
|
+
Otherwise they come from the model, and are stored in ``cache``.
|
|
288
|
+
"""
|
|
289
|
+
chunks = forward_chunks(runner, pieces, layer, batch_tokens) # on the ANE if it can
|
|
290
|
+
return chunks if cache is None else cache.through(runner, layer, pieces, batch_tokens, chunks)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class TopExamples:
|
|
294
|
+
"""Collects every piece's strengths and keeps the ``k`` strongest that don't overlap."""
|
|
295
|
+
|
|
296
|
+
def __init__(self) -> None:
|
|
297
|
+
self.candidates: list[tuple[Piece, list[int], list[float]]] = []
|
|
298
|
+
|
|
299
|
+
def update(self, batch: Batch, strengths: torch.Tensor) -> None:
|
|
300
|
+
"""Add a batch's pieces as candidates, with their real tokens' strengths."""
|
|
301
|
+
for row, piece in enumerate(batch.pieces):
|
|
302
|
+
mask = batch.mask[row]
|
|
303
|
+
self.add(piece, batch.ids[row][mask].tolist(), strengths[row][mask].tolist())
|
|
304
|
+
|
|
305
|
+
def update_chunk(self, chunk: Chunk, strengths: torch.Tensor) -> None:
|
|
306
|
+
"""Add a chunk's pieces as candidates, with their strengths."""
|
|
307
|
+
for piece, rows in chunk.rows():
|
|
308
|
+
self.add(piece, chunk.ids[rows].tolist(), strengths[rows].tolist())
|
|
309
|
+
|
|
310
|
+
def add(self, piece: Piece, ids: list[int], values: list[float]) -> None:
|
|
311
|
+
"""Add one piece as a candidate, unless it has no strengths."""
|
|
312
|
+
if values:
|
|
313
|
+
self.candidates.append((piece, ids, values))
|
|
314
|
+
|
|
315
|
+
def top(
|
|
316
|
+
self, tokenizer, k: int, skip_first: bool = False, labels: TokenLabels | None = None
|
|
317
|
+
) -> list[ActivatingExample]:
|
|
318
|
+
"""Return the ``k`` strongest pieces by their peak.
|
|
319
|
+
|
|
320
|
+
With ``skip_first``, the peak is after the first token. Each has its peak token's
|
|
321
|
+
``labels``.
|
|
322
|
+
"""
|
|
323
|
+
start = int(skip_first)
|
|
324
|
+
ranked = [(max(v[start:]), p, i, v) for p, i, v in self.candidates if len(v) > start]
|
|
325
|
+
examples: list[ActivatingExample] = []
|
|
326
|
+
chosen: list[Piece] = []
|
|
327
|
+
for peak_value, piece, row_ids, row_vals in sorted(ranked, key=lambda c: -c[0]):
|
|
328
|
+
if len(examples) == k:
|
|
329
|
+
break
|
|
330
|
+
if any(piece.overlaps(c) for c in chosen):
|
|
331
|
+
continue
|
|
332
|
+
chosen.append(piece)
|
|
333
|
+
peak = row_vals.index(peak_value, start)
|
|
334
|
+
examples.append(
|
|
335
|
+
ActivatingExample(
|
|
336
|
+
text=piece.text,
|
|
337
|
+
tokens=[tokenizer.decode([i]) for i in row_ids],
|
|
338
|
+
activations=[round(v, 5) for v in row_vals],
|
|
339
|
+
max_activation=peak_value,
|
|
340
|
+
max_token_index=peak,
|
|
341
|
+
peak_labels=labels.at(piece, row_ids, peak) if labels else None,
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
return examples
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def scan_neuron(
|
|
348
|
+
runner: NeuronHookRunner,
|
|
349
|
+
texts: Sequence[str],
|
|
350
|
+
layer: int,
|
|
351
|
+
neuron_idx: int,
|
|
352
|
+
pole: Pole,
|
|
353
|
+
*,
|
|
354
|
+
k: int = 20,
|
|
355
|
+
batch_tokens: int = BATCH_TOKENS,
|
|
356
|
+
n_correlated: int = 10,
|
|
357
|
+
min_count: int = 3,
|
|
358
|
+
window_tokens: int | None = None,
|
|
359
|
+
stride: int | None = None,
|
|
360
|
+
cache: LayerCache | None = None,
|
|
361
|
+
opposite_k: int = 5,
|
|
362
|
+
taggers: Sequence[Tagger] = (),
|
|
363
|
+
) -> ScanResult:
|
|
364
|
+
"""Scan ``texts`` for one neuron pole, and for its other pole.
|
|
365
|
+
|
|
366
|
+
The other pole gets its top ``opposite_k`` examples and histogram. That is free,
|
|
367
|
+
because the pass reads the whole activation. With ``cache``, replay the layer's stored
|
|
368
|
+
activations if it holds this scan, and store them if not. Each of ``taggers`` labels
|
|
369
|
+
the counted tokens, and its confound joins the profile's.
|
|
370
|
+
"""
|
|
371
|
+
if runner.tokenizer is None:
|
|
372
|
+
raise ValueError("scan_neuron requires a tokenizer")
|
|
373
|
+
other: Pole = "negative" if pole == "positive" else "positive"
|
|
374
|
+
pieces = list(cut_pieces(runner.tokenizer, texts, window_tokens, stride))
|
|
375
|
+
|
|
376
|
+
def run(): # fresh accumulators, so a pass redone on torch starts from nothing
|
|
377
|
+
labels = LabelPass(runner.tokenizer, texts, taggers)
|
|
378
|
+
poles = (pole, other)
|
|
379
|
+
return labels, _pole_pass(
|
|
380
|
+
runner, pieces, layer, neuron_idx, poles, batch_tokens, cache, labels
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
labels, scan = with_ane_fallback(runner, run)
|
|
384
|
+
tops = (scan.top, scan.other_top)
|
|
385
|
+
rescore_scan(
|
|
386
|
+
runner, pieces, layer, neuron_idx, (pole, other), tops, (k, opposite_k), batch_tokens
|
|
387
|
+
)
|
|
388
|
+
read, read_other = (_reader(runner, layer, neuron_idx, p) for p in (pole, other))
|
|
389
|
+
tokens = labels.tokens
|
|
390
|
+
examples, values, floor = settle(runner, read, scan.values, scan.first, scan.top, k, tokens)
|
|
391
|
+
other_examples, other_values, other_floor = settle(
|
|
392
|
+
runner, read_other, scan.other_values, scan.first, scan.other_top, opposite_k, tokens
|
|
393
|
+
)
|
|
394
|
+
# "Active" for co-activation uses the protocol's own definition for the target.
|
|
395
|
+
active = ReviewPolicy().activation_threshold * max(values, default=0.0)
|
|
396
|
+
confounds = labels.finalize(scan.confounds.finalize(runner.tokenizer, min_count), values)
|
|
397
|
+
return ScanResult(
|
|
398
|
+
examples=examples,
|
|
399
|
+
values=values,
|
|
400
|
+
correlated=scan.partners.partners(neuron_idx, active, k=n_correlated) if active > 0 else [],
|
|
401
|
+
confounds=confounds,
|
|
402
|
+
windows=len(pieces),
|
|
403
|
+
opposite=OppositeScan(other, other_examples, other_values, other_floor),
|
|
404
|
+
first_token_floor=floor,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@dataclass
|
|
409
|
+
class _PolePass:
|
|
410
|
+
"""What one pass over the pieces gathers for a neuron's pole and its other pole."""
|
|
411
|
+
|
|
412
|
+
partners: PartnerSearch
|
|
413
|
+
confounds: ConfoundAccumulator = field(default_factory=ConfoundAccumulator)
|
|
414
|
+
top: TopExamples = field(default_factory=TopExamples)
|
|
415
|
+
other_top: TopExamples = field(default_factory=TopExamples)
|
|
416
|
+
values: list[float] = field(default_factory=list)
|
|
417
|
+
other_values: list[float] = field(default_factory=list)
|
|
418
|
+
first: list[bool] = field(default_factory=list)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _pole_pass(runner, pieces, layer: int, neuron_idx: int, poles, batch_tokens, cache, labels):
|
|
422
|
+
pole, other = poles
|
|
423
|
+
scan = _PolePass(PartnerSearch(runner.mlp_width(layer)))
|
|
424
|
+
for chunk in scan_chunks(runner, pieces, layer, batch_tokens, cache):
|
|
425
|
+
acts = chunk.acts.float() # [n, d_mlp]
|
|
426
|
+
strengths = pole_strength(acts[:, neuron_idx], pole)
|
|
427
|
+
counted = chunk.counted
|
|
428
|
+
scan.partners.update(strengths[counted], acts[counted])
|
|
429
|
+
scan.confounds.update(chunk.ids[counted], chunk.positions[counted], strengths[counted])
|
|
430
|
+
labels.update_chunk(chunk, strengths)
|
|
431
|
+
scan.values.extend(strengths[counted].tolist())
|
|
432
|
+
scan.first.extend(chunk.first[counted].tolist())
|
|
433
|
+
scan.top.update_chunk(chunk, strengths)
|
|
434
|
+
other_strengths = pole_strength(acts[:, neuron_idx], other)
|
|
435
|
+
scan.other_values.extend(other_strengths[counted].tolist())
|
|
436
|
+
scan.other_top.update_chunk(chunk, other_strengths)
|
|
437
|
+
return scan
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _reader(runner, layer: int, neuron_idx: int, pole: Pole) -> Strengths:
|
|
441
|
+
from openinterp.harness.first_position import NeuronReader
|
|
442
|
+
|
|
443
|
+
return NeuronReader(runner, layer, neuron_idx, pole)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def top_activating_examples(
|
|
447
|
+
runner: NeuronHookRunner,
|
|
448
|
+
texts: Sequence[str],
|
|
449
|
+
layer: int,
|
|
450
|
+
neuron_idx: int,
|
|
451
|
+
pole: Pole,
|
|
452
|
+
*,
|
|
453
|
+
k: int = 20,
|
|
454
|
+
batch_tokens: int = BATCH_TOKENS,
|
|
455
|
+
) -> tuple[list[ActivatingExample], list[float]]:
|
|
456
|
+
"""Scan ``texts``; return the ``k`` strongest snippets and every token's pole strength.
|
|
457
|
+
|
|
458
|
+
A thin wrapper over :func:`scan_neuron`, which also gathers correlated neurons and
|
|
459
|
+
the confound profile.
|
|
460
|
+
"""
|
|
461
|
+
result = scan_neuron(runner, texts, layer, neuron_idx, pole, k=k, batch_tokens=batch_tokens)
|
|
462
|
+
return result.examples, result.values
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Where a scan's forward pass runs, and the exact measurement of its top candidates.
|
|
2
|
+
|
|
3
|
+
A runner can carry a ``scan_backend``, an :class:`~openinterp.harness.ane.backend.AneScan`.
|
|
4
|
+
It runs the scan's forward pass on Apple's Neural Engine (ANE). The device planner
|
|
5
|
+
(``harness/devices.py``) adds it when the GPU is busy. The ANE computes in fp16, about 1%
|
|
6
|
+
from fp32. So after the pass, :func:`rescore_scan` measures each pole's strongest
|
|
7
|
+
candidates again on the runner's own device:
|
|
8
|
+
|
|
9
|
+
- the :data:`RESCORE_TOP` strongest pieces;
|
|
10
|
+
- and any piece within :data:`RESCORE_MARGIN` of the 20th strongest, up to
|
|
11
|
+
:data:`RESCORE_CAP` pieces.
|
|
12
|
+
|
|
13
|
+
On ``corpus.jsonl`` that rule found the exact top 20 for every neuron and both poles. The
|
|
14
|
+
layers were GPT-2 L5, Qwen2.5-0.5B L12 and Qwen2.5-1.5B L8 (docs/analyses/ane-feasibility.md).
|
|
15
|
+
Then it makes sure that every example the scan will keep was measured again. The
|
|
16
|
+
numbers that ``open-interpretability reproduce`` checks are then the device's numbers. The
|
|
17
|
+
histogram, partners and confounds keep the ANE's values, which correlate with fp32 at
|
|
18
|
+
r ≥ 0.9998 for every neuron.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
import click
|
|
27
|
+
import torch
|
|
28
|
+
|
|
29
|
+
from openinterp.harness.hooks import pole_strength
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from openinterp.core.schemas import Pole
|
|
33
|
+
from openinterp.harness.hooks import NeuronHookRunner
|
|
34
|
+
from openinterp.harness.scan import Chunk, Piece, TopExamples
|
|
35
|
+
|
|
36
|
+
RESCORE_TOP = 60
|
|
37
|
+
RESCORE_MARGIN = 0.06
|
|
38
|
+
RESCORE_CAP = 120
|
|
39
|
+
RESCORE_RANK = 20 # the margin is below this rank's peak
|
|
40
|
+
RESCORE_BATCH = 16 # pieces per forward pass when measuring again
|
|
41
|
+
RESCORE_ROUNDS = 5 # rounds that add examples the selection would keep unmeasured
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ane_scan(runner: NeuronHookRunner, pieces: Sequence[Piece], layer: int, batch_tokens: int):
|
|
45
|
+
"""The runner's ANE backend, made ready for this scan, if it can run it; else None.
|
|
46
|
+
|
|
47
|
+
If it can't, it says why on stderr, once for each reason.
|
|
48
|
+
"""
|
|
49
|
+
backend = getattr(runner, "scan_backend", None)
|
|
50
|
+
if backend is None or not pieces:
|
|
51
|
+
return None
|
|
52
|
+
every = runner.tokenizer([p.text for p in pieces], return_tensors="pt", padding=True)
|
|
53
|
+
mask = every.get("attention_mask", torch.ones_like(every["input_ids"]))
|
|
54
|
+
reason = backend.prepare(mask.sum(1).tolist(), layer, batch_tokens)
|
|
55
|
+
if reason is None:
|
|
56
|
+
return backend
|
|
57
|
+
if reason not in backend.noted:
|
|
58
|
+
backend.noted.add(reason)
|
|
59
|
+
click.echo(f"This scan runs on {runner.device.type}, not the ANE: {reason}.", err=True)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AneError(RuntimeError):
|
|
64
|
+
"""The ANE failed during a scan. The scan is then done again on torch."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def with_ane_fallback(runner: NeuronHookRunner, run: Callable):
|
|
68
|
+
"""``run()``; if the ANE fails during it, stop using the ANE and ``run()`` again.
|
|
69
|
+
|
|
70
|
+
The second run starts from nothing, so no partial result of the ANE is kept.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
return run()
|
|
74
|
+
except AneError as exc:
|
|
75
|
+
backend = runner.scan_backend
|
|
76
|
+
backend.broken = f"it failed during a scan ({exc.__cause__ or exc})"
|
|
77
|
+
click.echo(
|
|
78
|
+
f"The ANE failed, so this scan runs again on {runner.device.type}: {exc}", err=True
|
|
79
|
+
)
|
|
80
|
+
return run()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def scan_device(runner: NeuronHookRunner, pieces: Sequence[Piece], layer, batch_tokens) -> str:
|
|
84
|
+
"""The device a scan of ``pieces`` runs on: the backend's name, or the runner's device."""
|
|
85
|
+
backend = ane_scan(runner, pieces, layer, batch_tokens)
|
|
86
|
+
return backend.name if backend is not None else runner.device.type
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def forward_chunks(
|
|
90
|
+
runner: NeuronHookRunner, pieces: Sequence[Piece], layer: int, batch_tokens: int
|
|
91
|
+
) -> Iterator[Chunk]:
|
|
92
|
+
"""``layer``'s chunks over ``pieces``, from the ANE if it can run them, else torch."""
|
|
93
|
+
from openinterp.harness.scan import layer_chunks
|
|
94
|
+
|
|
95
|
+
backend = ane_scan(runner, pieces, layer, batch_tokens)
|
|
96
|
+
if backend is None:
|
|
97
|
+
return layer_chunks(runner, pieces, layer, batch_tokens)
|
|
98
|
+
backend.used = True
|
|
99
|
+
return backend.chunks(runner, pieces, layer, batch_tokens)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def rescore_scan(
|
|
103
|
+
runner, pieces, layer: int, neuron_idx: int, poles, tops, ks, batch_tokens
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Measure again the top candidates of each pole, if the scan's values are the ANE's.
|
|
106
|
+
|
|
107
|
+
``poles``, ``tops`` and ``ks`` are each pole, its :class:`TopExamples` and the number
|
|
108
|
+
of examples it keeps.
|
|
109
|
+
"""
|
|
110
|
+
if ane_scan(runner, pieces, layer, batch_tokens) is None:
|
|
111
|
+
return
|
|
112
|
+
for pole, top, k in zip(poles, tops, ks, strict=True):
|
|
113
|
+
rescore(top, _measure(runner, layer, neuron_idx, pole), k)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _measure(runner, layer: int, neuron_idx: int, pole: Pole) -> Callable:
|
|
117
|
+
"""A function that measures the pole's strengths on texts, :data:`RESCORE_BATCH` a pass.
|
|
118
|
+
|
|
119
|
+
On a model that streams, the passes run layer-major (``harness/layer_major.py``), so
|
|
120
|
+
each layer is read from disk once for all of them.
|
|
121
|
+
"""
|
|
122
|
+
from openinterp.harness.layer_major import map_layer_major, prompt_tokens
|
|
123
|
+
|
|
124
|
+
def one(texts: list[str]) -> list[list[float]]:
|
|
125
|
+
encoded = runner.encode(texts)
|
|
126
|
+
mask = encoded.get("attention_mask", torch.ones_like(encoded["input_ids"])).bool()
|
|
127
|
+
with torch.no_grad():
|
|
128
|
+
acts = runner.layer_activations(encoded, [layer])[layer][..., neuron_idx]
|
|
129
|
+
values = pole_strength(acts.float(), pole).cpu()
|
|
130
|
+
return [values[r][mask[r].cpu()].tolist() for r in range(len(texts))]
|
|
131
|
+
|
|
132
|
+
def measure(texts: list[str]) -> list[list[float]]:
|
|
133
|
+
parts = [texts[i : i + RESCORE_BATCH] for i in range(0, len(texts), RESCORE_BATCH)]
|
|
134
|
+
tokens = RESCORE_BATCH * prompt_tokens(runner, texts)
|
|
135
|
+
done = map_layer_major(runner, one, parts, tokens=tokens, logits=False)
|
|
136
|
+
return [values for part in done for values in part]
|
|
137
|
+
|
|
138
|
+
return measure
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def rescore(top: TopExamples, measure: Callable[[list[str]], list[list[float]]], k: int) -> int:
|
|
142
|
+
"""Replace the values of ``top``'s strongest candidates with ``measure``'s.
|
|
143
|
+
|
|
144
|
+
Return how many candidates it measured again.
|
|
145
|
+
"""
|
|
146
|
+
done: set[int] = set()
|
|
147
|
+
todo = chosen(top.candidates)
|
|
148
|
+
for _ in range(RESCORE_ROUNDS):
|
|
149
|
+
todo -= done
|
|
150
|
+
if not todo:
|
|
151
|
+
break
|
|
152
|
+
order = sorted(todo)
|
|
153
|
+
new = measure([top.candidates[i][0].text for i in order]) # all of them at once
|
|
154
|
+
for i, values in zip(order, new, strict=True):
|
|
155
|
+
piece, ids, old = top.candidates[i]
|
|
156
|
+
if len(values) != len(old):
|
|
157
|
+
raise ValueError(f"measured {len(values)} tokens of a {len(old)}-token piece")
|
|
158
|
+
top.candidates[i] = (piece, ids, values)
|
|
159
|
+
done |= todo
|
|
160
|
+
todo = kept(top.candidates, k)
|
|
161
|
+
return len(done)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def chosen(candidates: list) -> set[int]:
|
|
165
|
+
"""Indices of the candidates to measure again, by their peak with and without token 0."""
|
|
166
|
+
picked: set[int] = set()
|
|
167
|
+
for start in (0, 1):
|
|
168
|
+
peaks = torch.tensor(
|
|
169
|
+
[max(v[start:]) if len(v) > start else -torch.inf for *_, v in candidates]
|
|
170
|
+
)
|
|
171
|
+
order = peaks.argsort(descending=True)
|
|
172
|
+
ranked = peaks[order]
|
|
173
|
+
if not len(ranked):
|
|
174
|
+
continue
|
|
175
|
+
nth = ranked[min(RESCORE_RANK, len(ranked)) - 1]
|
|
176
|
+
line = nth - RESCORE_MARGIN * nth.abs() - 1e-3 * ranked[0].abs()
|
|
177
|
+
for rank, index in enumerate(order.tolist()):
|
|
178
|
+
if rank < RESCORE_TOP or (ranked[rank] >= line and rank < RESCORE_CAP):
|
|
179
|
+
picked.add(index)
|
|
180
|
+
return picked
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def kept(candidates: list, k: int) -> set[int]:
|
|
184
|
+
"""Indices of the candidates that the top ``k`` would keep, with and without token 0.
|
|
185
|
+
|
|
186
|
+
As ``TopExamples.top``, it skips a piece that overlaps one it kept.
|
|
187
|
+
"""
|
|
188
|
+
keep: set[int] = set()
|
|
189
|
+
for start in (0, 1):
|
|
190
|
+
ranked = sorted(
|
|
191
|
+
(i for i, (*_, v) in enumerate(candidates) if len(v) > start),
|
|
192
|
+
key=lambda i: -max(candidates[i][2][start:]),
|
|
193
|
+
)
|
|
194
|
+
taken: list[int] = []
|
|
195
|
+
for i in ranked:
|
|
196
|
+
if len(taken) == k:
|
|
197
|
+
break
|
|
198
|
+
if not any(candidates[i][0].overlaps(candidates[j][0]) for j in taken):
|
|
199
|
+
taken.append(i)
|
|
200
|
+
keep.update(taken)
|
|
201
|
+
return keep
|