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,200 @@
|
|
|
1
|
+
"""Measure the multi-dimensional feature check (:class:`~openinterp.core.manifold.ManifoldCheck`).
|
|
2
|
+
|
|
3
|
+
The axes are the audited pole and its top partner poles. Each axis is divided by its
|
|
4
|
+
peak over every token of the top examples. So a partner with large activations doesn't
|
|
5
|
+
fill the first component. Unit variance would be worse: it would blow a partner that is
|
|
6
|
+
silent on a family, with tiny jitter, up into a full axis.
|
|
7
|
+
|
|
8
|
+
* **The activating set.** Each top example is run again, and the poles are read at the
|
|
9
|
+
audited pole's peak.
|
|
10
|
+
* **The families.** Each word goes into every carrier sentence of
|
|
11
|
+
:data:`openinterp.harness.specificity.CARRIERS`. A pole's strength on the word is its
|
|
12
|
+
peak over the word's tokens, as a mean over the carriers.
|
|
13
|
+
|
|
14
|
+
A circle's score follows Engels et al. (2024). Their first component is often an
|
|
15
|
+
intensity direction and the circle lies in components 2 and 3. So the score takes the
|
|
16
|
+
better of the planes 1–2 and 2–3. The p-value repeats that choice on each shuffled
|
|
17
|
+
order, so it pays for the choice.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import cmath
|
|
23
|
+
import math
|
|
24
|
+
from collections.abc import Sequence
|
|
25
|
+
|
|
26
|
+
import torch
|
|
27
|
+
|
|
28
|
+
from openinterp.core.manifold import FamilyManifold, ManifoldCheck, ManifoldPole
|
|
29
|
+
from openinterp.core.schemas import ACTIVATION_THRESHOLD, ActivatingExample, CorrelatedNeuron
|
|
30
|
+
from openinterp.core.stats import spearman
|
|
31
|
+
from openinterp.harness.hooks import NeuronHookRunner, pole_strength
|
|
32
|
+
from openinterp.harness.specificity import CARRIERS
|
|
33
|
+
|
|
34
|
+
PARTNERS = 5 # partner poles beside the audited pole
|
|
35
|
+
SHOWN = 3 # principal components whose explained variance is recorded
|
|
36
|
+
SHUFFLES = 999 # shuffled orders for each p-value
|
|
37
|
+
FAMILIES: dict[str, tuple[str, tuple[str, ...]]] = {
|
|
38
|
+
"weekdays": ("circle", ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
|
|
39
|
+
"Saturday", "Sunday")),
|
|
40
|
+
"months": ("circle", ("January", "February", "March", "April", "May", "June", "July",
|
|
41
|
+
"August", "September", "October", "November", "December")),
|
|
42
|
+
"numbers": ("line", ("one", "two", "three", "four", "five", "six", "seven", "eight",
|
|
43
|
+
"nine", "ten", "eleven", "twelve")),
|
|
44
|
+
} # fmt: skip
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def axes(neuron_idx: int, pole, partners: Sequence[CorrelatedNeuron]) -> list[ManifoldPole]:
|
|
48
|
+
"""The audited pole, then its top :data:`PARTNERS` partner poles."""
|
|
49
|
+
own = ManifoldPole(neuron_idx=neuron_idx, pole=pole)
|
|
50
|
+
return [own, *(ManifoldPole(neuron_idx=c.neuron_idx, pole=c.pole) for c in partners)][
|
|
51
|
+
: 1 + PARTNERS
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _strengths(acts: torch.Tensor, poles: Sequence[ManifoldPole]) -> torch.Tensor:
|
|
56
|
+
"""``[seq, len(poles)]``: each pole's strength at each token, from layer ``acts``."""
|
|
57
|
+
columns = [pole_strength(acts[:, p.neuron_idx], p.pole) for p in poles]
|
|
58
|
+
return torch.stack(columns, dim=1).float().cpu()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _layer(runner: NeuronHookRunner, text: str, layer: int) -> torch.Tensor:
|
|
62
|
+
"""``[seq, width]``: the MLP hidden activations of ``layer`` on ``text``."""
|
|
63
|
+
return runner.layer_activations(text, [layer])[layer][0]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def word_vector(runner, layer: int, poles, word: str) -> torch.Tensor | None:
|
|
67
|
+
"""Each pole's peak over ``word``'s tokens, as a mean over the carriers.
|
|
68
|
+
|
|
69
|
+
``None`` when no carrier measures a token of the word.
|
|
70
|
+
"""
|
|
71
|
+
found = []
|
|
72
|
+
for carrier in CARRIERS:
|
|
73
|
+
text, start = carrier.format(word), carrier.index("{}")
|
|
74
|
+
offsets = runner.tokenizer(text, return_offsets_mapping=True)["offset_mapping"]
|
|
75
|
+
values = _strengths(_layer(runner, text, layer), poles)
|
|
76
|
+
held = [i for i, (a, b) in enumerate(offsets) if a < start + len(word) and b > start]
|
|
77
|
+
if len(offsets) == len(values) and held:
|
|
78
|
+
found.append(values[held].max(dim=0).values)
|
|
79
|
+
return torch.stack(found).mean(dim=0) if found else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def peak_vector(runner, layer: int, poles, example: ActivatingExample, skip_first: bool):
|
|
83
|
+
"""The poles at the audited pole's peak, and each pole's peak over the example.
|
|
84
|
+
|
|
85
|
+
The example's text is run again.
|
|
86
|
+
"""
|
|
87
|
+
values = _strengths(_layer(runner, example.text, layer), poles)
|
|
88
|
+
start = int(skip_first and len(values) > 1)
|
|
89
|
+
return values[start + int(values[start:, 0].argmax())], values[start:].max(dim=0).values
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def scaled(rows: torch.Tensor, scales: torch.Tensor) -> torch.Tensor:
|
|
93
|
+
"""``rows`` with each pole divided by its scale; a pole with no scale is 0."""
|
|
94
|
+
return torch.where(scales > 0, rows / scales.clamp_min(1e-12), torch.zeros_like(rows))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def pca(rows: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
98
|
+
"""The coordinates, the variances and the unit components of the centred ``rows``."""
|
|
99
|
+
z = rows.double() - rows.double().mean(dim=0)
|
|
100
|
+
u, s, vt = torch.linalg.svd(z, full_matrices=False)
|
|
101
|
+
return u * s, s**2, vt
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def ratios(variances: torch.Tensor) -> list[float]:
|
|
105
|
+
"""The first :data:`SHOWN` shares of variance, or none when nothing varies."""
|
|
106
|
+
total = float(variances.sum())
|
|
107
|
+
return [round(float(v) / total, 4) for v in variances[:SHOWN]] if total > 0 else []
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def participation_ratio(variances: torch.Tensor) -> float:
|
|
111
|
+
"""``(Σλ)² / Σλ²``: the effective number of dimensions; 0 when nothing varies."""
|
|
112
|
+
squares = float((variances**2).sum())
|
|
113
|
+
return round(float(variances.sum()) ** 2 / squares, 4) if squares > 0 else 0.0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def circle_score(coords: torch.Tensor, places: Sequence[int], n: int) -> float:
|
|
117
|
+
"""The mean resultant length of angle minus cycle place, in the better direction."""
|
|
118
|
+
angles = [math.atan2(float(y), float(x)) for x, y in coords]
|
|
119
|
+
cycle = [2 * math.pi * k / n for k in places]
|
|
120
|
+
forward = abs(sum(cmath.exp(1j * (a - c)) for a, c in zip(angles, cycle, strict=True)))
|
|
121
|
+
backward = abs(sum(cmath.exp(1j * (a + c)) for a, c in zip(angles, cycle, strict=True)))
|
|
122
|
+
return max(forward, backward) / len(angles)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _best(shape: str, coords: torch.Tensor, order: Sequence[int]) -> tuple[float, list[int]]:
|
|
126
|
+
"""The best score over the allowed components, and those components (1-based).
|
|
127
|
+
|
|
128
|
+
On a tie the first components win. With no components the score is 0.
|
|
129
|
+
"""
|
|
130
|
+
if shape == "circle":
|
|
131
|
+
planes = [[0, 1], [1, 2]][: max(coords.shape[1] - 1, 0)]
|
|
132
|
+
found = [(circle_score(coords[:, p], order, len(order)), p) for p in planes]
|
|
133
|
+
else:
|
|
134
|
+
values = torch.tensor(order, dtype=torch.float64)
|
|
135
|
+
found = [
|
|
136
|
+
(abs(spearman(coords[:, k], values)), [k]) for k in range(min(SHOWN, coords.shape[1]))
|
|
137
|
+
]
|
|
138
|
+
score, picked = max(found, key=lambda f: f[0], default=(0.0, []))
|
|
139
|
+
return score, [k + 1 for k in picked]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def family_manifold(
|
|
143
|
+
family: str, rows: torch.Tensor, threshold: float, scales: torch.Tensor | None = None
|
|
144
|
+
) -> FamilyManifold:
|
|
145
|
+
"""The PCA and shape of one family's ``rows`` (members in order, poles as columns).
|
|
146
|
+
|
|
147
|
+
``threshold`` is in the audited pole's units. The PCA sees ``rows`` divided by
|
|
148
|
+
``scales`` (:func:`scaled`), when given.
|
|
149
|
+
"""
|
|
150
|
+
shape = FAMILIES[family][0]
|
|
151
|
+
coords, variances, vt = pca(rows if scales is None else scaled(rows, scales))
|
|
152
|
+
order = list(range(len(rows)))
|
|
153
|
+
if not float(variances.sum()) > 0: # nothing varies: no shape and no components
|
|
154
|
+
coords = coords[:, :0]
|
|
155
|
+
score, components = _best(shape, coords, order)
|
|
156
|
+
rng = torch.Generator().manual_seed(len(rows)) # fixed: a record always replays
|
|
157
|
+
beaten = sum(
|
|
158
|
+
_best(shape, coords, torch.randperm(len(rows), generator=rng).tolist())[0] >= score - 1e-9
|
|
159
|
+
for _ in range(SHUFFLES)
|
|
160
|
+
)
|
|
161
|
+
share = float((vt[[c - 1 for c in components], 0] ** 2).sum()) if components else 0.0
|
|
162
|
+
plane = sorted((float(variances[c - 1]) for c in components), reverse=True)
|
|
163
|
+
round_ = plane[1] / plane[0] if shape == "circle" and len(plane) == 2 and plane[0] > 0 else None
|
|
164
|
+
return FamilyManifold(
|
|
165
|
+
family=family, shape=shape, members=len(rows),
|
|
166
|
+
active_members=int((rows[:, 0] >= threshold).sum()),
|
|
167
|
+
explained_variance=ratios(variances), components=components,
|
|
168
|
+
score=round(min(score, 1.0), 4), p_value=round((1 + beaten) / (1 + SHUFFLES), 4),
|
|
169
|
+
pole_share=round(min(share, 1.0), 4),
|
|
170
|
+
roundness=None if round_ is None else round(round_, 4),
|
|
171
|
+
) # fmt: skip
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def manifold_check(
|
|
175
|
+
runner: NeuronHookRunner,
|
|
176
|
+
layer: int,
|
|
177
|
+
poles: list[ManifoldPole],
|
|
178
|
+
examples: Sequence[ActivatingExample],
|
|
179
|
+
skip_first: bool = False,
|
|
180
|
+
) -> ManifoldCheck:
|
|
181
|
+
"""Measure the check for ``poles[0]`` and its partners in ``layer``."""
|
|
182
|
+
max_act = max((e.max_activation for e in examples), default=0.0)
|
|
183
|
+
measured = [peak_vector(runner, layer, poles, e, skip_first) for e in examples]
|
|
184
|
+
peaks = [p for p, _ in measured]
|
|
185
|
+
scales = torch.stack([s for _, s in measured]).max(dim=0).values if measured else None
|
|
186
|
+
variances = pca(scaled(torch.stack(peaks), scales))[1] if len(peaks) > 1 else torch.zeros(0)
|
|
187
|
+
families = []
|
|
188
|
+
for family, (_, words) in FAMILIES.items():
|
|
189
|
+
vectors = [word_vector(runner, layer, poles, w) for w in words]
|
|
190
|
+
if all(v is not None for v in vectors):
|
|
191
|
+
rows = torch.stack(vectors)
|
|
192
|
+
threshold = ACTIVATION_THRESHOLD * max_act
|
|
193
|
+
families.append(family_manifold(family, rows, threshold, scales))
|
|
194
|
+
return ManifoldCheck(
|
|
195
|
+
poles=poles,
|
|
196
|
+
activating_examples=len(peaks),
|
|
197
|
+
activating_explained_variance=ratios(variances),
|
|
198
|
+
participation_ratio=participation_ratio(variances),
|
|
199
|
+
families=families,
|
|
200
|
+
)
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""Streaming mode: run a model larger than this machine's memory, slowly.
|
|
2
|
+
|
|
3
|
+
In streaming mode the decoder layers stay in the checkpoint's safetensors files on disk.
|
|
4
|
+
``accelerate`` reads a layer's weights when the forward pass reaches it, and frees them
|
|
5
|
+
after. The embeddings, the final norm and the output head stay in memory. Peak memory is
|
|
6
|
+
then about one layer, those modules and one batch's activations. The cost is time: every
|
|
7
|
+
forward pass reads its layers from disk again.
|
|
8
|
+
|
|
9
|
+
It is never the default for a model that fits. ``--memory auto`` (the default) streams
|
|
10
|
+
only when the weights at the chosen dtype, plus :data:`WORKING_MARGIN_BYTES`, exceed free
|
|
11
|
+
RAM plus free swap. ``--memory stream`` forces it, and ``--memory normal`` never streams.
|
|
12
|
+
The maths is the same, so a streamed record reproduces in normal mode.
|
|
13
|
+
|
|
14
|
+
A streamed scan reads its layers once per batch, so it uses larger batches
|
|
15
|
+
(:func:`stream_batch_tokens`) to read each layer fewer times.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from collections.abc import Iterator, Mapping
|
|
22
|
+
from contextlib import AbstractContextManager, contextmanager, nullcontext, suppress
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Literal
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
import torch
|
|
29
|
+
from torch import nn
|
|
30
|
+
|
|
31
|
+
MemoryMode = Literal["auto", "normal", "stream"]
|
|
32
|
+
MEMORY_MODES: tuple[MemoryMode, ...] = ("auto", "normal", "stream")
|
|
33
|
+
|
|
34
|
+
# What a scan or review holds beyond the weights, measured on CPU in float32. It is about
|
|
35
|
+
# 2.5–3 GB for GPT-2 and Qwen2.5-0.5B and 1.5B (docs/experiments.md, Memory).
|
|
36
|
+
WORKING_MARGIN_BYTES = 3 * 2**30
|
|
37
|
+
|
|
38
|
+
# A streamed scan's batch: as many tokens as its activations fit in, within these bounds.
|
|
39
|
+
# The floor is the normal scan's batch (harness/scan.py, BATCH_TOKENS).
|
|
40
|
+
STREAM_BATCH_TOKENS_MAX = 65536
|
|
41
|
+
|
|
42
|
+
# The most rows a streamed pass of single prompts runs at once (:func:`stream_rows`).
|
|
43
|
+
STREAM_ROWS_MAX = 64
|
|
44
|
+
|
|
45
|
+
MEMORY_ENV = "OPENINTERP_MEMORY_GB" # the memory to plan for, in place of what is free
|
|
46
|
+
|
|
47
|
+
_requested: MemoryMode = "auto"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def request(mode: MemoryMode) -> None:
|
|
51
|
+
"""Set the mode that the next model load uses (the ``--memory`` option)."""
|
|
52
|
+
global _requested
|
|
53
|
+
_requested = mode
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def requested() -> MemoryMode:
|
|
57
|
+
"""The mode that ``--memory`` asked for."""
|
|
58
|
+
return _requested
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class MemoryPlan:
|
|
63
|
+
"""Whether to stream a model, and the numbers that decided it."""
|
|
64
|
+
|
|
65
|
+
stream: bool
|
|
66
|
+
need_bytes: int | None # weights at the chosen dtype, plus the working margin
|
|
67
|
+
available_bytes: int | None # free RAM plus free swap
|
|
68
|
+
reason: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def available_bytes(device: str | torch.device | None = None) -> int | None:
|
|
72
|
+
"""Free memory to plan for on ``device``, or ``None`` when it can't be read.
|
|
73
|
+
|
|
74
|
+
On a CUDA device that is the GPU's free memory. Elsewhere it is free RAM plus free
|
|
75
|
+
swap. ``device`` defaults to the one the last model loaded on. ``$OPENINTERP_MEMORY_GB``
|
|
76
|
+
overrides it, to run as if only that much were free: on a shared machine, or to
|
|
77
|
+
test streaming on a large one.
|
|
78
|
+
"""
|
|
79
|
+
budget = os.environ.get(MEMORY_ENV)
|
|
80
|
+
if budget:
|
|
81
|
+
return int(float(budget) * 2**30)
|
|
82
|
+
device = torch.device(device if device is not None else _budget_device)
|
|
83
|
+
try:
|
|
84
|
+
if device.type == "cuda":
|
|
85
|
+
return int(torch.cuda.mem_get_info(device)[0])
|
|
86
|
+
import psutil
|
|
87
|
+
|
|
88
|
+
return int(psutil.virtual_memory().available + psutil.swap_memory().free)
|
|
89
|
+
except Exception: # psutil missing, an unsupported platform, or no CUDA
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_budget_device = "cpu" # where the last model was loaded: what available_bytes plans for
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def checkpoint_bytes(snapshot: Path) -> int:
|
|
97
|
+
"""Total size of the safetensors (else PyTorch) weight files in a model snapshot."""
|
|
98
|
+
files = list(snapshot.glob("*.safetensors")) or list(snapshot.glob("*.bin"))
|
|
99
|
+
return sum(f.stat().st_size for f in files)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def weights_bytes(snapshot: Path, config, dtype: torch.dtype | str) -> int:
|
|
103
|
+
"""The weights' size in memory at ``dtype``: the checkpoint scaled from its own dtype."""
|
|
104
|
+
stored = getattr(config, "torch_dtype", None) or getattr(config, "dtype", None)
|
|
105
|
+
size = checkpoint_bytes(snapshot)
|
|
106
|
+
if dtype == "auto" or stored is None:
|
|
107
|
+
return size
|
|
108
|
+
stored = getattr(torch, stored) if isinstance(stored, str) else stored
|
|
109
|
+
return size * _itemsize(dtype) // _itemsize(stored)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def plan(
|
|
113
|
+
mode: MemoryMode, need: int | None, have: int | None, where: str = "RAM and swap"
|
|
114
|
+
) -> MemoryPlan:
|
|
115
|
+
"""Decide whether to stream: always for ``stream``, never for ``normal``.
|
|
116
|
+
|
|
117
|
+
For ``auto``, stream only when ``need`` is known to exceed ``have``, the free
|
|
118
|
+
memory ``where`` names.
|
|
119
|
+
"""
|
|
120
|
+
if mode == "stream":
|
|
121
|
+
return MemoryPlan(True, need, have, "--memory stream")
|
|
122
|
+
if mode == "normal" or need is None or have is None:
|
|
123
|
+
return MemoryPlan(False, need, have, f"--memory {mode}")
|
|
124
|
+
if need <= have:
|
|
125
|
+
return MemoryPlan(False, need, have, "the model fits")
|
|
126
|
+
reason = f"it needs about {_gb(need)} and {_gb(have)} is free ({where})"
|
|
127
|
+
return MemoryPlan(True, need, have, reason)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def warn(memory: MemoryPlan, model_name: str) -> None:
|
|
131
|
+
"""Say on stderr that ``model_name`` streams from disk, and why."""
|
|
132
|
+
click.echo(
|
|
133
|
+
f"Streaming {model_name} from disk, because {memory.reason}. This is much "
|
|
134
|
+
"slower: every forward pass reads its layers from disk. Pass --memory normal "
|
|
135
|
+
"to load it whole.",
|
|
136
|
+
err=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def load_causal_lm(model_name: str, device: str, dtype, revision: str | None, **extra):
|
|
141
|
+
"""Load ``model_name`` whole on ``device``, or streamed from disk (see the module doc).
|
|
142
|
+
|
|
143
|
+
A streamed model runs on the CPU. Its offloaded layers read the checkpoint files in
|
|
144
|
+
place, so streaming writes no copy of them. A mixture-of-experts checkpoint that
|
|
145
|
+
stores each expert apart is the exception. transformers stacks its experts as it
|
|
146
|
+
loads, and streaming writes the stacks to :func:`offload_dir`.
|
|
147
|
+
"""
|
|
148
|
+
from transformers import AutoModelForCausalLM
|
|
149
|
+
|
|
150
|
+
from openinterp.harness.moe import use_openinterp_experts
|
|
151
|
+
|
|
152
|
+
global _budget_device
|
|
153
|
+
memory = _plan_load(model_name, dtype, revision, device)
|
|
154
|
+
if not memory.stream:
|
|
155
|
+
kwargs = {"dtype": dtype, "revision": revision, **extra}
|
|
156
|
+
model = AutoModelForCausalLM.from_pretrained(model_name, **kwargs).to(device)
|
|
157
|
+
use_openinterp_experts(model)
|
|
158
|
+
_budget_device = device
|
|
159
|
+
return model
|
|
160
|
+
warn(memory, model_name)
|
|
161
|
+
resident = stream_device(device)
|
|
162
|
+
if resident != device:
|
|
163
|
+
click.echo(f"Streaming runs on the CPU, not {device}.", err=True)
|
|
164
|
+
empty = _empty_model(model_name, revision)
|
|
165
|
+
device_map = stream_device_map(empty, _layers_path(empty, model_name), resident)
|
|
166
|
+
model = AutoModelForCausalLM.from_pretrained(
|
|
167
|
+
model_name,
|
|
168
|
+
dtype=dtype,
|
|
169
|
+
revision=revision,
|
|
170
|
+
device_map=device_map,
|
|
171
|
+
offload_folder=str(offload_dir()),
|
|
172
|
+
**extra,
|
|
173
|
+
)
|
|
174
|
+
keep_checkpoint_open(model, resident)
|
|
175
|
+
use_openinterp_experts(model) # reads only the routed experts (harness/moe.py)
|
|
176
|
+
_budget_device = resident
|
|
177
|
+
return model
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def stream_device(device: str) -> str:
|
|
181
|
+
"""Where a streamed model runs: ``device`` if it is CUDA, else the CPU.
|
|
182
|
+
|
|
183
|
+
On CUDA the embeddings, the final norm and the head stay on the GPU. Each layer is
|
|
184
|
+
read from disk and copied to the GPU when a pass reaches it. ``from_pretrained``
|
|
185
|
+
with a device map on MPS ends in a segmentation fault (transformers 5.20), so MPS
|
|
186
|
+
streams on the CPU.
|
|
187
|
+
"""
|
|
188
|
+
return device if torch.device(device).type == "cuda" else "cpu"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class OpenCheckpoint(Mapping):
|
|
192
|
+
"""``accelerate``'s offloaded weights, read from safetensors files that stay open.
|
|
193
|
+
|
|
194
|
+
``accelerate`` opens the checkpoint file again for each tensor it reads. On
|
|
195
|
+
Qwen2.5-0.5B that took about 19 ms a tensor, and a pass reads 288 tensors. So it was
|
|
196
|
+
5.6 s of a 5.9 s pass over 16 tokens. With each file opened once, the pass took
|
|
197
|
+
0.18 s, and 0.14 s whole. The tensors read are the same. An open file is
|
|
198
|
+
memory-mapped. The pages it reads are in the file cache, which the system can free.
|
|
199
|
+
They are not the process's own memory.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, loader, device: str = "cpu") -> None:
|
|
203
|
+
self.loader = loader # accelerate's OffloadedWeightsLoader
|
|
204
|
+
self.files: dict[str, object] = {}
|
|
205
|
+
self.device = torch.device(device) # where the layers run: tensors are sent there
|
|
206
|
+
|
|
207
|
+
def __getitem__(self, key: str) -> torch.Tensor:
|
|
208
|
+
info = self.loader.index.get(key)
|
|
209
|
+
if key in self.loader.state_dict or not info or not info.get("safetensors_file"):
|
|
210
|
+
return self.loader[key]
|
|
211
|
+
path = info["safetensors_file"]
|
|
212
|
+
if path not in self.files:
|
|
213
|
+
from safetensors import safe_open
|
|
214
|
+
|
|
215
|
+
self.files[path] = safe_open(path, framework="pt", device="cpu")
|
|
216
|
+
tensor = self.files[path].get_tensor(info.get("weight_name", key))
|
|
217
|
+
return self._sent(tensor, info)
|
|
218
|
+
|
|
219
|
+
def slice(self, key: str, index: int) -> torch.Tensor:
|
|
220
|
+
"""Row ``index`` of the stacked tensor ``key``, read without reading the rest.
|
|
221
|
+
|
|
222
|
+
A mixture-of-experts layer stacks its experts in one tensor. A pass reads only
|
|
223
|
+
the experts its tokens are routed to (:mod:`openinterp.harness.moe`).
|
|
224
|
+
"""
|
|
225
|
+
info = self.loader.index.get(key)
|
|
226
|
+
if key in self.loader.state_dict or not info or not info.get("safetensors_file"):
|
|
227
|
+
return self.loader[key][index]
|
|
228
|
+
path = info["safetensors_file"]
|
|
229
|
+
if path not in self.files:
|
|
230
|
+
from safetensors import safe_open
|
|
231
|
+
|
|
232
|
+
self.files[path] = safe_open(path, framework="pt", device="cpu")
|
|
233
|
+
row = self.files[path].get_slice(info.get("weight_name", key))[index]
|
|
234
|
+
return self._sent(row, info)
|
|
235
|
+
|
|
236
|
+
def _sent(self, tensor: torch.Tensor, info: dict) -> torch.Tensor:
|
|
237
|
+
"""``tensor`` on the layers' device, then at its dtype.
|
|
238
|
+
|
|
239
|
+
On a GPU it is sent in the checkpoint's dtype and converted there. Take a float32
|
|
240
|
+
run of Qwen2.5-0.5B, whose checkpoint is bfloat16. A streamed pass then took 0.66 s
|
|
241
|
+
on an RTX 4070 Ti, against 2.8 s converted on the CPU first.
|
|
242
|
+
"""
|
|
243
|
+
tensor = tensor.to(self.device)
|
|
244
|
+
return tensor.to(getattr(torch, info["dtype"])) if "dtype" in info else tensor
|
|
245
|
+
|
|
246
|
+
def __iter__(self) -> Iterator[str]:
|
|
247
|
+
return iter(self.loader)
|
|
248
|
+
|
|
249
|
+
def __len__(self) -> int:
|
|
250
|
+
return len(self.loader)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def keep_checkpoint_open(model: nn.Module, device: str = "cpu") -> None:
|
|
254
|
+
"""Make every streamed module of ``model`` read through one :class:`OpenCheckpoint`.
|
|
255
|
+
|
|
256
|
+
``device`` is where the layers run (:func:`stream_device`).
|
|
257
|
+
"""
|
|
258
|
+
from accelerate.utils import PrefixedDataset
|
|
259
|
+
|
|
260
|
+
opened: dict[int, OpenCheckpoint] = {}
|
|
261
|
+
for module in model.modules():
|
|
262
|
+
weights = getattr(getattr(module, "_hf_hook", None), "weights_map", None)
|
|
263
|
+
if isinstance(weights, PrefixedDataset) and hasattr(weights.dataset, "index"):
|
|
264
|
+
key = id(weights.dataset)
|
|
265
|
+
weights.dataset = opened.setdefault(key, OpenCheckpoint(weights.dataset, device))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def offload_dir() -> Path:
|
|
269
|
+
"""Where ``accelerate`` may write weights it can't read in place (a ``.bin`` checkpoint).
|
|
270
|
+
|
|
271
|
+
``$XDG_CACHE_HOME/openinterp/offload``, else ``~/.cache/openinterp/offload``. It stays
|
|
272
|
+
empty for a safetensors checkpoint.
|
|
273
|
+
"""
|
|
274
|
+
root = os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache"
|
|
275
|
+
return Path(root) / "openinterp" / "offload"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _plan_load(model_name: str, dtype, revision: str | None, device: str = "cpu") -> MemoryPlan:
|
|
279
|
+
"""The streaming decision for loading ``model_name`` at ``dtype`` on ``device``."""
|
|
280
|
+
mode = requested()
|
|
281
|
+
if mode == "normal":
|
|
282
|
+
return plan(mode, None, None)
|
|
283
|
+
need = None
|
|
284
|
+
# Offline, or an unknown layout: don't stream unless asked.
|
|
285
|
+
with suppress(Exception):
|
|
286
|
+
snapshot = _snapshot(model_name, revision)
|
|
287
|
+
from transformers import AutoConfig
|
|
288
|
+
|
|
289
|
+
config = AutoConfig.from_pretrained(model_name, revision=revision)
|
|
290
|
+
need = weights_bytes(snapshot, config, dtype) + WORKING_MARGIN_BYTES
|
|
291
|
+
on_gpu = torch.device(device).type == "cuda"
|
|
292
|
+
return plan(mode, need, available_bytes(device), "on the GPU" if on_gpu else "RAM and swap")
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _snapshot(model_name: str, revision: str | None) -> Path:
|
|
296
|
+
"""The local folder that holds ``model_name``'s weights, downloading them if needed."""
|
|
297
|
+
if Path(model_name).is_dir():
|
|
298
|
+
return Path(model_name)
|
|
299
|
+
from huggingface_hub import snapshot_download
|
|
300
|
+
|
|
301
|
+
patterns = ["*.json", "*.safetensors"]
|
|
302
|
+
return Path(snapshot_download(model_name, revision=revision, allow_patterns=patterns))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _empty_model(model_name: str, revision: str | None) -> nn.Module:
|
|
306
|
+
"""``model_name``'s architecture with every weight on the meta device (no memory)."""
|
|
307
|
+
from accelerate import init_empty_weights
|
|
308
|
+
from transformers import AutoConfig, AutoModelForCausalLM
|
|
309
|
+
|
|
310
|
+
config = AutoConfig.from_pretrained(model_name, revision=revision)
|
|
311
|
+
with init_empty_weights():
|
|
312
|
+
return AutoModelForCausalLM.from_config(config)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _layers_path(model: nn.Module, model_name: str) -> str:
|
|
316
|
+
"""The dotted path of ``model``'s decoder layers."""
|
|
317
|
+
from openinterp.harness.hooks import LAYER_PATHS, _try_path
|
|
318
|
+
|
|
319
|
+
for path in LAYER_PATHS:
|
|
320
|
+
layers = _try_path(model, path)
|
|
321
|
+
if isinstance(layers, nn.ModuleList) and len(layers) > 0:
|
|
322
|
+
return path
|
|
323
|
+
from openinterp.core.exceptions import ArchitectureNotSupportedError
|
|
324
|
+
|
|
325
|
+
raise ArchitectureNotSupportedError(f"no decoder layers found on {model_name}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def stream_device_map(model: nn.Module, layers_path: str, resident: str = "cpu") -> dict[str, str]:
|
|
329
|
+
"""An ``accelerate`` device map with each decoder layer on disk, the rest on ``resident``.
|
|
330
|
+
|
|
331
|
+
``model`` may be on the meta device. ``layers_path`` names its decoder layers.
|
|
332
|
+
``accelerate`` runs the layers on the first device in the map that isn't the CPU or
|
|
333
|
+
the disk. So they run on the GPU when ``resident`` is one.
|
|
334
|
+
"""
|
|
335
|
+
device_map: dict[str, str] = {}
|
|
336
|
+
|
|
337
|
+
def assign(module: nn.Module, prefix: str) -> None:
|
|
338
|
+
for name, child in module.named_children():
|
|
339
|
+
path = f"{prefix}{name}"
|
|
340
|
+
if path == layers_path:
|
|
341
|
+
device_map.update({f"{path}.{i}": "disk" for i in range(len(child))})
|
|
342
|
+
elif layers_path.startswith(path + "."):
|
|
343
|
+
assign(child, path + ".")
|
|
344
|
+
else:
|
|
345
|
+
device_map[path] = resident
|
|
346
|
+
|
|
347
|
+
assign(model, "")
|
|
348
|
+
return device_map
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def stream_batch_tokens(config, dtype: torch.dtype, have: int | None, floor: int) -> int:
|
|
352
|
+
"""A streamed scan's batch: the tokens whose activations fit in half of ``have``.
|
|
353
|
+
|
|
354
|
+
One token of one layer holds about 4 residual-width and 3 MLP-width vectors at once
|
|
355
|
+
(the residual, attention and MLP inputs and outputs). The result is between
|
|
356
|
+
``floor`` and :data:`STREAM_BATCH_TOKENS_MAX`.
|
|
357
|
+
"""
|
|
358
|
+
width = getattr(config, "hidden_size", None) or getattr(config, "n_embd", None)
|
|
359
|
+
inner = getattr(config, "intermediate_size", None) or getattr(config, "n_inner", None)
|
|
360
|
+
if not width or have is None:
|
|
361
|
+
return floor
|
|
362
|
+
per_token = _itemsize(dtype) * (4 * width + 3 * (inner or 4 * width))
|
|
363
|
+
tokens = (have // 2) // per_token
|
|
364
|
+
return int(max(floor, min(STREAM_BATCH_TOKENS_MAX, tokens)))
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def stream_rows(runner, tokens_per_row: int, default: int, *, logits: bool) -> int:
|
|
368
|
+
"""Rows per forward pass: ``default``, or more when ``runner``'s model streams.
|
|
369
|
+
|
|
370
|
+
Every streamed pass reads its layers from disk again. So a streamed model runs as
|
|
371
|
+
many rows at once as a quarter of the planned memory holds (:func:`available_bytes`).
|
|
372
|
+
A pass that returns ``logits`` holds them and one log-softmax copy: 1.2 MB a token at
|
|
373
|
+
Qwen2.5's 151,936 tokens. Rows are independent, so only float rounding changes. The
|
|
374
|
+
result is at least ``default`` and at most :data:`STREAM_ROWS_MAX`.
|
|
375
|
+
"""
|
|
376
|
+
have = available_bytes()
|
|
377
|
+
if not is_streamed(runner.model) or have is None:
|
|
378
|
+
return default
|
|
379
|
+
have //= _sharing # passes that run at once share the memory (layer_major)
|
|
380
|
+
config = runner.model.config
|
|
381
|
+
dtype = next(runner.model.parameters()).dtype
|
|
382
|
+
width = getattr(config, "hidden_size", None) or getattr(config, "n_embd", 1)
|
|
383
|
+
inner = getattr(config, "intermediate_size", None) or getattr(config, "n_inner", None)
|
|
384
|
+
per_token = _itemsize(dtype) * (4 * width + 3 * (inner or 4 * width))
|
|
385
|
+
if logits:
|
|
386
|
+
per_token += 2 * 4 * getattr(config, "vocab_size", 0) # float32 logits and log-probs
|
|
387
|
+
rows = (have // 4) // max(1, per_token * max(1, tokens_per_row))
|
|
388
|
+
return int(max(default, min(STREAM_ROWS_MAX, rows)))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
_sharing = 1 # how many passes run at once (openinterp.harness.layer_major)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@contextmanager
|
|
395
|
+
def shared_by(passes: int) -> Iterator[None]:
|
|
396
|
+
"""Plan each pass's memory as a share of the budget while ``passes`` run at once."""
|
|
397
|
+
global _sharing
|
|
398
|
+
previous, _sharing = _sharing, max(1, passes)
|
|
399
|
+
try:
|
|
400
|
+
yield
|
|
401
|
+
finally:
|
|
402
|
+
_sharing = previous
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def loaded(module: nn.Module) -> AbstractContextManager:
|
|
406
|
+
"""A context in which ``module``'s own weights are real tensors, also when streamed.
|
|
407
|
+
|
|
408
|
+
A streamed module keeps its weights on the meta device between passes. Read them
|
|
409
|
+
inside this context, and copy what must outlive it. Child modules are not loaded.
|
|
410
|
+
"""
|
|
411
|
+
if getattr(module, "_hf_hook", None) is None:
|
|
412
|
+
return nullcontext()
|
|
413
|
+
from accelerate.utils import align_module_device
|
|
414
|
+
|
|
415
|
+
return align_module_device(module)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@contextmanager
|
|
419
|
+
def recompute_below(runner, layer: int) -> Iterator[None]:
|
|
420
|
+
"""In a streamed model, recompute the layers below ``layer`` in a backward pass.
|
|
421
|
+
|
|
422
|
+
Autograd keeps the weights a layer used until the backward pass. So a gradient
|
|
423
|
+
through streamed layers holds all of them at once. Inside this block each layer
|
|
424
|
+
below ``layer`` keeps only its input. The backward pass runs it again, which reads
|
|
425
|
+
its weights from disk again. The values are the same. Take Qwen2.5-1.5B at layer
|
|
426
|
+
11, with frozen weights (``cli._load``). Integrated gradients over 1,024 tokens
|
|
427
|
+
peaked at 1.8 GB with this, and at 4.5 GB without. It took 5.2 s, not 4.2 s.
|
|
428
|
+
|
|
429
|
+
``layer`` itself isn't recomputed, since a hook there may end the forward pass. The
|
|
430
|
+
key-value cache is off, since a recomputed layer would add to it twice. A model that
|
|
431
|
+
isn't streamed is left as it is.
|
|
432
|
+
"""
|
|
433
|
+
if not is_streamed(runner.model):
|
|
434
|
+
yield
|
|
435
|
+
return
|
|
436
|
+
from torch.utils.checkpoint import checkpoint
|
|
437
|
+
|
|
438
|
+
def recomputed(forward):
|
|
439
|
+
def run(*args, **kwargs):
|
|
440
|
+
if not torch.is_grad_enabled():
|
|
441
|
+
return forward(*args, **kwargs)
|
|
442
|
+
return checkpoint(forward, *args, use_reentrant=False, **kwargs)
|
|
443
|
+
|
|
444
|
+
return run
|
|
445
|
+
|
|
446
|
+
config = runner.model.config
|
|
447
|
+
use_cache, config.use_cache = getattr(config, "use_cache", None), False
|
|
448
|
+
blocks = list(runner.layers)[:layer]
|
|
449
|
+
forwards = [block.forward for block in blocks]
|
|
450
|
+
for block, forward in zip(blocks, forwards, strict=True):
|
|
451
|
+
block.forward = recomputed(forward)
|
|
452
|
+
try:
|
|
453
|
+
yield
|
|
454
|
+
finally:
|
|
455
|
+
for block, forward in zip(blocks, forwards, strict=True):
|
|
456
|
+
block.forward = forward
|
|
457
|
+
config.use_cache = use_cache
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def is_streamed(model: nn.Module) -> bool:
|
|
461
|
+
"""Whether some of ``model``'s weights stay on disk between passes."""
|
|
462
|
+
return any(p.device.type == "meta" for p in model.parameters())
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _itemsize(dtype: torch.dtype | str) -> int:
|
|
466
|
+
dtype = getattr(torch, dtype) if isinstance(dtype, str) else dtype
|
|
467
|
+
return torch.empty((), dtype=dtype).element_size()
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _gb(n: int) -> str:
|
|
471
|
+
return f"{n / 2**30:.1f} GB"
|