logogram 0.1.0__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.
- logogram/__init__.py +6 -0
- logogram/__main__.py +5 -0
- logogram/analysis.py +419 -0
- logogram/atp.py +120 -0
- logogram/backends/__init__.py +5 -0
- logogram/backends/base.py +202 -0
- logogram/backends/hub.py +375 -0
- logogram/backends/saes.py +277 -0
- logogram/backends/transformer_lens.py +872 -0
- logogram/cli.py +496 -0
- logogram/compare.py +177 -0
- logogram/datasets.py +159 -0
- logogram/direct.py +193 -0
- logogram/engine.py +550 -0
- logogram/examples/ioi-gpt2/.gitignore +3 -0
- logogram/examples/ioi-gpt2/datasets/ioi.jsonl +32 -0
- logogram/examples/ioi-gpt2/experiments/ioi-head-patching/spec.json +42 -0
- logogram/examples/ioi-gpt2/project.json +6 -0
- logogram/exports.py +33 -0
- logogram/features.py +368 -0
- logogram/fileio.py +63 -0
- logogram/ioi.py +220 -0
- logogram/paths.py +204 -0
- logogram/project.py +444 -0
- logogram/prompts.py +204 -0
- logogram/research.py +84 -0
- logogram/results.py +240 -0
- logogram/runner.py +396 -0
- logogram/runs.py +98 -0
- logogram/sae.py +161 -0
- logogram/schema.py +302 -0
- logogram/server/__init__.py +1 -0
- logogram/server/app.py +1083 -0
- logogram/server/models.py +426 -0
- logogram/server/security.py +212 -0
- logogram/server/state.py +585 -0
- logogram/sites.py +249 -0
- logogram/spec.py +518 -0
- logogram/stats.py +171 -0
- logogram/steering.py +258 -0
- logogram/system.py +379 -0
- logogram/updates.py +194 -0
- logogram/verify.py +39 -0
- logogram/web_dist/assets/index-BvCU-2uy.js +54 -0
- logogram/web_dist/assets/index-DTr8_ucV.css +1 -0
- logogram/web_dist/assets/instrument-sans-latin-ext-standard-normal-C5E2Gvlv.woff2 +0 -0
- logogram/web_dist/assets/instrument-sans-latin-standard-normal-BVScPF0l.woff2 +0 -0
- logogram/web_dist/favicon.svg +1 -0
- logogram/web_dist/index.html +15 -0
- logogram-0.1.0.dist-info/METADATA +550 -0
- logogram-0.1.0.dist-info/RECORD +54 -0
- logogram-0.1.0.dist-info/WHEEL +4 -0
- logogram-0.1.0.dist-info/entry_points.txt +2 -0
- logogram-0.1.0.dist-info/licenses/LICENSE +21 -0
logogram/steering.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Steering: add a direction to the residual stream at several strengths, and measure the effect.
|
|
2
|
+
|
|
3
|
+
At each steered site (a residual stream state at one token), the direction is the mean of
|
|
4
|
+
(other prompt - receiver prompt) over a training split of the pairs. Adding it at strength 1 to a
|
|
5
|
+
held-out receiver prompt moves that prompt's activation by the average difference between the two
|
|
6
|
+
prompts; the effect is normalized like patching, so 1 means the output moved as far as switching
|
|
7
|
+
to the other prompt. Directions are never applied to the prompts that computed them. A random
|
|
8
|
+
direction of the same length, at the same strengths, is the control: a direction that matters
|
|
9
|
+
should beat it.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import threading
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import torch
|
|
20
|
+
|
|
21
|
+
from logogram.backends.base import Cancelled, ModelBackend, ModelInfo, Patch
|
|
22
|
+
from logogram.engine import (
|
|
23
|
+
EngineError,
|
|
24
|
+
EngineResult,
|
|
25
|
+
LayerFn,
|
|
26
|
+
ProgressFn,
|
|
27
|
+
_answer_tensors,
|
|
28
|
+
_chunks,
|
|
29
|
+
_LayerSources,
|
|
30
|
+
_metric,
|
|
31
|
+
check_gap,
|
|
32
|
+
compute_baselines,
|
|
33
|
+
)
|
|
34
|
+
from logogram.prompts import PreparedPrompt, group_by_length
|
|
35
|
+
from logogram.sites import (
|
|
36
|
+
ResolvedSite,
|
|
37
|
+
ScopeError,
|
|
38
|
+
_check_site,
|
|
39
|
+
resolve_position,
|
|
40
|
+
site_label,
|
|
41
|
+
)
|
|
42
|
+
from logogram.spec import (
|
|
43
|
+
AllPositions,
|
|
44
|
+
LayerComponentsScope,
|
|
45
|
+
Site,
|
|
46
|
+
SitesScope,
|
|
47
|
+
Spec,
|
|
48
|
+
Steering,
|
|
49
|
+
strength_text,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
RESIDUAL = ("resid_pre", "resid_mid", "resid_post")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def split_pairs(n: int, train_fraction: float, seed: int) -> tuple[list[int], list[int]]:
|
|
56
|
+
"""Positions of the pairs that train the direction and of the held-out pairs it is measured
|
|
57
|
+
on: a seeded shuffle from the raw PCG64 stream, so the split is the same on every machine."""
|
|
58
|
+
n_train = int(round(n * train_fraction))
|
|
59
|
+
if n_train < 1 or n - n_train < 2:
|
|
60
|
+
raise EngineError(
|
|
61
|
+
f"Steering needs at least one prompt pair to compute the direction and two to measure "
|
|
62
|
+
f"it on, but {n} pairs with a training share of {train_fraction:.0%} leaves "
|
|
63
|
+
f"{n_train} and {n - n_train}. Use more prompts or another training share."
|
|
64
|
+
)
|
|
65
|
+
order = list(range(n))
|
|
66
|
+
raw = np.random.PCG64(seed).random_raw(n)
|
|
67
|
+
for t in range(n - 1, 0, -1):
|
|
68
|
+
j = int(raw[t] % np.uint64(t + 1))
|
|
69
|
+
order[t], order[j] = order[j], order[t]
|
|
70
|
+
return sorted(order[:n_train]), sorted(order[n_train:])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def steered_sites(spec: Spec, info: ModelInfo, prompts: list[PreparedPrompt]) -> list[Site]:
|
|
74
|
+
"""The residual-stream sites a steering spec adds its direction at, one token per prompt."""
|
|
75
|
+
scope = spec.scope
|
|
76
|
+
if isinstance(scope, LayerComponentsScope):
|
|
77
|
+
if len(scope.components) != 1 or scope.components[0] not in RESIDUAL:
|
|
78
|
+
raise ScopeError(
|
|
79
|
+
"Steering adds a direction to the residual stream: choose exactly one residual "
|
|
80
|
+
"site (before, between or after the layer's blocks)."
|
|
81
|
+
)
|
|
82
|
+
sites = [
|
|
83
|
+
Site(kind=scope.components[0], layer=layer, position=scope.position)
|
|
84
|
+
for layer in range(info.n_layers)
|
|
85
|
+
]
|
|
86
|
+
elif isinstance(scope, SitesScope):
|
|
87
|
+
sites = list(scope.sites)
|
|
88
|
+
if any(s.kind not in RESIDUAL for s in sites):
|
|
89
|
+
raise ScopeError(
|
|
90
|
+
"Steering adds a direction to the residual stream; heads, attention outputs and "
|
|
91
|
+
"MLP outputs can't be steered here. Choose residual sites."
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
raise ScopeError(
|
|
95
|
+
"Steering runs at one residual site in every layer, or at chosen residual sites. "
|
|
96
|
+
"Choose “Attention and MLP per layer” with a single residual component, or pick sites."
|
|
97
|
+
)
|
|
98
|
+
for site in sites:
|
|
99
|
+
if isinstance(site.position, AllPositions):
|
|
100
|
+
raise ScopeError(
|
|
101
|
+
"Steering adds the direction at one token of each prompt. Choose the last token, a "
|
|
102
|
+
"named position or a token index."
|
|
103
|
+
)
|
|
104
|
+
_check_site(site, info)
|
|
105
|
+
for prompt in prompts:
|
|
106
|
+
resolve_position(site.position, prompt)
|
|
107
|
+
return sites
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def run_steering(
|
|
111
|
+
spec: Spec,
|
|
112
|
+
backend: ModelBackend,
|
|
113
|
+
prompts: list[PreparedPrompt],
|
|
114
|
+
*,
|
|
115
|
+
on_progress: ProgressFn | None = None,
|
|
116
|
+
on_layer: LayerFn | None = None,
|
|
117
|
+
cancel: threading.Event | None = None,
|
|
118
|
+
on_start: Callable[[list[ResolvedSite], dict[str, Any]], None] | None = None,
|
|
119
|
+
) -> EngineResult:
|
|
120
|
+
exp = spec.experiment
|
|
121
|
+
assert isinstance(exp, Steering)
|
|
122
|
+
info = backend.info
|
|
123
|
+
batch_size = spec.execution.batch_size
|
|
124
|
+
base = steered_sites(spec, info, prompts)
|
|
125
|
+
train_at, test_at = split_pairs(len(prompts), exp.train_fraction, exp.seed)
|
|
126
|
+
train = [prompts[i] for i in train_at]
|
|
127
|
+
test = [prompts[i] for i in test_at]
|
|
128
|
+
receiver = exp.apply_to
|
|
129
|
+
reference = "corrupt" if receiver == "clean" else "clean"
|
|
130
|
+
|
|
131
|
+
# The variants of each site: every strength along the direction, then along the control.
|
|
132
|
+
variants = [(c, False) for c in exp.coefficients]
|
|
133
|
+
if exp.control:
|
|
134
|
+
variants += [(c, True) for c in exp.coefficients]
|
|
135
|
+
keys = [
|
|
136
|
+
f"random {strength_text(c)}" if control else strength_text(c) for c, control in variants
|
|
137
|
+
]
|
|
138
|
+
uniform = len({(s.kind, s.position.model_dump_json()) for s in base}) == 1
|
|
139
|
+
sites: list[ResolvedSite] = []
|
|
140
|
+
for b, site in enumerate(base):
|
|
141
|
+
for v, ((coefficient, control), key) in enumerate(zip(variants, keys, strict=True)):
|
|
142
|
+
sites.append(
|
|
143
|
+
ResolvedSite(
|
|
144
|
+
index=len(sites),
|
|
145
|
+
site=site,
|
|
146
|
+
row=b,
|
|
147
|
+
col=v,
|
|
148
|
+
label=f"{site_label(site)} {key}",
|
|
149
|
+
variant={"coefficient": coefficient, "control": control},
|
|
150
|
+
variant_key=key,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
layout = {
|
|
154
|
+
"kind": "steering",
|
|
155
|
+
"site": base[0].kind if uniform else None,
|
|
156
|
+
"row_title": "Layer" if uniform else "Site",
|
|
157
|
+
"col_title": "Strength",
|
|
158
|
+
"rows": [
|
|
159
|
+
{"key": str(b), "label": str(s.layer) if uniform else site_label(s)}
|
|
160
|
+
for b, s in enumerate(base)
|
|
161
|
+
],
|
|
162
|
+
"cols": [
|
|
163
|
+
{"key": key, "label": key, "coefficient": c, "control": control}
|
|
164
|
+
for (c, control), key in zip(variants, keys, strict=True)
|
|
165
|
+
],
|
|
166
|
+
}
|
|
167
|
+
if on_start is not None:
|
|
168
|
+
on_start(sites, layout)
|
|
169
|
+
|
|
170
|
+
test_groups = group_by_length(test)
|
|
171
|
+
train_groups = group_by_length(train)
|
|
172
|
+
baselines = compute_baselines(backend, test, test_groups, batch_size, cancel)
|
|
173
|
+
warnings = check_gap(spec, baselines, test, receiver, reference)
|
|
174
|
+
n = len(test)
|
|
175
|
+
patched_ld = np.zeros((len(sites), n))
|
|
176
|
+
patched_prob = np.zeros((len(sites), n))
|
|
177
|
+
result = EngineResult(
|
|
178
|
+
sites=sites,
|
|
179
|
+
layout=layout,
|
|
180
|
+
prompts=test,
|
|
181
|
+
baselines=baselines,
|
|
182
|
+
receiver=receiver,
|
|
183
|
+
reference=reference,
|
|
184
|
+
patched_ld=patched_ld,
|
|
185
|
+
patched_prob=patched_prob,
|
|
186
|
+
warnings=warnings,
|
|
187
|
+
extra={
|
|
188
|
+
"steering": {
|
|
189
|
+
"train": [p.index for p in train],
|
|
190
|
+
"test": [p.index for p in test],
|
|
191
|
+
"norms": [],
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
model_dtype = {"float32": torch.float32, "float16": torch.float16, "bfloat16": torch.bfloat16}
|
|
196
|
+
dtype = model_dtype[info.dtype]
|
|
197
|
+
local_of = {i: li for g in test_groups for li, i in enumerate(g.members)}
|
|
198
|
+
total = len(sites) * n
|
|
199
|
+
done = 0
|
|
200
|
+
for b, site in enumerate(base):
|
|
201
|
+
probe = ResolvedSite(0, site, 0, 0, "")
|
|
202
|
+
kinds = [site.kind]
|
|
203
|
+
# The direction: mean (reference - receiver) over the training pairs, at the site's token.
|
|
204
|
+
toward = _LayerSources(
|
|
205
|
+
backend, train, train_groups, site.layer, kinds, reference, batch_size, cancel
|
|
206
|
+
)
|
|
207
|
+
away = _LayerSources(
|
|
208
|
+
backend, train, train_groups, site.layer, kinds, receiver, batch_size, cancel
|
|
209
|
+
)
|
|
210
|
+
direction = (
|
|
211
|
+
toward.table(site.kind, probe).double() - away.table(site.kind, probe).double()
|
|
212
|
+
).mean(0)
|
|
213
|
+
norm = float(direction.norm())
|
|
214
|
+
result.extra["steering"]["norms"].append(norm)
|
|
215
|
+
control_direction = None
|
|
216
|
+
if exp.control:
|
|
217
|
+
generator = torch.Generator().manual_seed(exp.seed * 1_000_003 + b)
|
|
218
|
+
noise = torch.randn(direction.shape, generator=generator, dtype=torch.float64)
|
|
219
|
+
control_direction = noise / noise.norm() * norm
|
|
220
|
+
# The held-out receivers' own activations, to which the direction is added.
|
|
221
|
+
own = _LayerSources(
|
|
222
|
+
backend, test, test_groups, site.layer, kinds, receiver, batch_size, cancel
|
|
223
|
+
).table(site.kind, probe)
|
|
224
|
+
positions = [resolve_position(site.position, p) for p in test]
|
|
225
|
+
first = b * len(variants)
|
|
226
|
+
for group in test_groups:
|
|
227
|
+
tokens = group.clean if receiver == "clean" else group.corrupt
|
|
228
|
+
rows = [(v, p) for v in range(len(variants)) for p in group.members]
|
|
229
|
+
for sl in _chunks(len(rows), batch_size):
|
|
230
|
+
if cancel is not None and cancel.is_set():
|
|
231
|
+
raise Cancelled()
|
|
232
|
+
chunk = rows[sl]
|
|
233
|
+
values = torch.stack(
|
|
234
|
+
[
|
|
235
|
+
own[p].double()
|
|
236
|
+
+ variants[v][0]
|
|
237
|
+
* (control_direction if variants[v][1] else direction).to(own.device)
|
|
238
|
+
for v, p in chunk
|
|
239
|
+
]
|
|
240
|
+
).to(dtype)
|
|
241
|
+
patch = Patch(
|
|
242
|
+
kind=site.kind,
|
|
243
|
+
layer=site.layer,
|
|
244
|
+
values=values,
|
|
245
|
+
positions=torch.tensor([positions[p] for _, p in chunk], dtype=torch.long),
|
|
246
|
+
)
|
|
247
|
+
local = torch.tensor([local_of[p] for _, p in chunk], dtype=torch.long)
|
|
248
|
+
logits = backend.final_logits(tokens[local], patch)
|
|
249
|
+
ld, prob = _metric(logits, *_answer_tensors(test, [p for _, p in chunk]))
|
|
250
|
+
for (v, p), value_ld, value_prob in zip(chunk, ld, prob, strict=True):
|
|
251
|
+
patched_ld[first + v, p] = value_ld
|
|
252
|
+
patched_prob[first + v, p] = value_prob
|
|
253
|
+
done += len(chunk)
|
|
254
|
+
if on_progress is not None:
|
|
255
|
+
on_progress(done, total, site.layer)
|
|
256
|
+
if on_layer is not None:
|
|
257
|
+
on_layer(site.layer, list(range(first, first + len(variants))), result)
|
|
258
|
+
return result
|
logogram/system.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""Hardware and environment report, with concrete fixes for common problems.
|
|
2
|
+
|
|
3
|
+
Used by the first-run system check, by ``logogram doctor`` and by the memory estimate. Nothing
|
|
4
|
+
here is written to disk or sent anywhere.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import functools
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import asdict, dataclass, field
|
|
17
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import psutil
|
|
22
|
+
|
|
23
|
+
GB = 1024**3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Issue:
|
|
28
|
+
severity: str # "error" | "warning" | "info"
|
|
29
|
+
title: str
|
|
30
|
+
detail: str
|
|
31
|
+
fix: str | None = None # a command or a short instruction
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class GPU:
|
|
36
|
+
name: str
|
|
37
|
+
memory_total: int
|
|
38
|
+
memory_free: int | None
|
|
39
|
+
bf16: bool
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class SystemReport:
|
|
44
|
+
os: str
|
|
45
|
+
machine: str
|
|
46
|
+
python: str
|
|
47
|
+
cpu: str
|
|
48
|
+
cpu_cores: int | None
|
|
49
|
+
cpu_threads: int | None
|
|
50
|
+
memory_total: int
|
|
51
|
+
memory_available: int
|
|
52
|
+
gpus: list[GPU]
|
|
53
|
+
backend: str # cuda | mps | cpu
|
|
54
|
+
torch: str
|
|
55
|
+
torch_cuda: str | None
|
|
56
|
+
transformer_lens: str | None
|
|
57
|
+
transformers: str | None
|
|
58
|
+
recommended_dtype: str
|
|
59
|
+
precision_note: str
|
|
60
|
+
hf_cache_free: int | None
|
|
61
|
+
issues: list[Issue] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
def to_dict(self) -> dict[str, Any]:
|
|
64
|
+
return asdict(self)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _pkg(name: str) -> str | None:
|
|
68
|
+
try:
|
|
69
|
+
return version(name)
|
|
70
|
+
except PackageNotFoundError:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@functools.cache
|
|
75
|
+
def cpu_name() -> str:
|
|
76
|
+
system = platform.system()
|
|
77
|
+
try:
|
|
78
|
+
if system == "Linux":
|
|
79
|
+
for line in Path("/proc/cpuinfo").read_text().splitlines():
|
|
80
|
+
if line.lower().startswith("model name"):
|
|
81
|
+
return line.split(":", 1)[1].strip()
|
|
82
|
+
elif system == "Darwin":
|
|
83
|
+
out = subprocess.run(
|
|
84
|
+
["sysctl", "-n", "machdep.cpu.brand_string"],
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
timeout=2,
|
|
88
|
+
)
|
|
89
|
+
if out.stdout.strip():
|
|
90
|
+
return out.stdout.strip()
|
|
91
|
+
elif system == "Windows":
|
|
92
|
+
name = platform.processor()
|
|
93
|
+
if name:
|
|
94
|
+
return name
|
|
95
|
+
except Exception: # noqa: BLE001
|
|
96
|
+
pass
|
|
97
|
+
return platform.processor() or platform.machine() or "Unknown CPU"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _os_label() -> str:
|
|
101
|
+
system = platform.system()
|
|
102
|
+
if system == "Darwin":
|
|
103
|
+
return f"macOS {platform.mac_ver()[0]}"
|
|
104
|
+
if system == "Windows":
|
|
105
|
+
return f"Windows {platform.release()}"
|
|
106
|
+
if system == "Linux":
|
|
107
|
+
try:
|
|
108
|
+
info = platform.freedesktop_os_release()
|
|
109
|
+
return info.get("PRETTY_NAME") or info.get("NAME") or "Linux"
|
|
110
|
+
except OSError:
|
|
111
|
+
return "Linux"
|
|
112
|
+
return system or "Unknown"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def nvidia_driver_present() -> bool:
|
|
116
|
+
if Path("/proc/driver/nvidia/version").exists():
|
|
117
|
+
return True
|
|
118
|
+
return shutil.which("nvidia-smi") is not None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _rosetta() -> bool:
|
|
122
|
+
if platform.system() != "Darwin" or platform.machine() != "x86_64":
|
|
123
|
+
return False
|
|
124
|
+
try:
|
|
125
|
+
out = subprocess.run(
|
|
126
|
+
["sysctl", "-n", "sysctl.proc_translated"], capture_output=True, text=True, timeout=2
|
|
127
|
+
)
|
|
128
|
+
return out.stdout.strip() == "1"
|
|
129
|
+
except Exception: # noqa: BLE001
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def installed_as_uv_tool() -> bool:
|
|
134
|
+
return f"uv{os.sep}tools{os.sep}logogram" in sys.prefix.replace("/", os.sep)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def reinstall_command(backend: str) -> str:
|
|
138
|
+
if installed_as_uv_tool():
|
|
139
|
+
return f"uv tool install --reinstall --torch-backend={backend} logogram"
|
|
140
|
+
return f"uv pip install --reinstall torch --torch-backend={backend}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def hf_cache_dir() -> Path:
|
|
144
|
+
from huggingface_hub import constants
|
|
145
|
+
|
|
146
|
+
return Path(constants.HF_HUB_CACHE)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def system_report() -> SystemReport:
|
|
150
|
+
import torch
|
|
151
|
+
|
|
152
|
+
vm = psutil.virtual_memory()
|
|
153
|
+
gpus: list[GPU] = []
|
|
154
|
+
backend = "cpu"
|
|
155
|
+
if torch.cuda.is_available():
|
|
156
|
+
backend = "cuda"
|
|
157
|
+
for i in range(torch.cuda.device_count()):
|
|
158
|
+
props = torch.cuda.get_device_properties(i)
|
|
159
|
+
free = None
|
|
160
|
+
with contextlib.suppress(Exception):
|
|
161
|
+
free, _ = torch.cuda.mem_get_info(i)
|
|
162
|
+
gpus.append(
|
|
163
|
+
GPU(
|
|
164
|
+
name=props.name,
|
|
165
|
+
memory_total=int(props.total_memory),
|
|
166
|
+
memory_free=int(free) if free is not None else None,
|
|
167
|
+
bf16=bool(torch.cuda.is_bf16_supported()),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
elif getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
|
|
171
|
+
backend = "mps"
|
|
172
|
+
gpus.append(
|
|
173
|
+
GPU(name="Apple GPU (Metal)", memory_total=int(vm.total), memory_free=None, bf16=True)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
issues: list[Issue] = []
|
|
177
|
+
torch_cuda = torch.version.cuda
|
|
178
|
+
if backend != "cuda" and nvidia_driver_present():
|
|
179
|
+
if torch_cuda is None:
|
|
180
|
+
issues.append(
|
|
181
|
+
Issue(
|
|
182
|
+
"warning",
|
|
183
|
+
"Your NVIDIA GPU isn't being used",
|
|
184
|
+
"This PyTorch build is CPU-only, so experiments run on the CPU. Reinstall "
|
|
185
|
+
"PyTorch with CUDA support to use the GPU.",
|
|
186
|
+
reinstall_command("auto"),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
issues.append(
|
|
191
|
+
Issue(
|
|
192
|
+
"warning",
|
|
193
|
+
"PyTorch can't reach your NVIDIA GPU",
|
|
194
|
+
f"PyTorch was built for CUDA {torch_cuda}, but the NVIDIA driver can't run it. "
|
|
195
|
+
"Update the NVIDIA driver, or install a PyTorch build for an older CUDA "
|
|
196
|
+
"version (for example cu126).",
|
|
197
|
+
reinstall_command("cu126"),
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
if platform.system() == "Darwin" and backend == "cpu":
|
|
201
|
+
if _rosetta():
|
|
202
|
+
issues.append(
|
|
203
|
+
Issue(
|
|
204
|
+
"warning",
|
|
205
|
+
"Python is running under Rosetta",
|
|
206
|
+
"This is an Intel (x86_64) Python on Apple Silicon, so the GPU (MPS) can't be "
|
|
207
|
+
"used. Install a native arm64 Python and reinstall Logogram with it.",
|
|
208
|
+
"uv python install 3.12 && uv tool install --reinstall --python 3.12 logogram",
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
elif platform.machine() == "arm64":
|
|
212
|
+
issues.append(
|
|
213
|
+
Issue(
|
|
214
|
+
"warning",
|
|
215
|
+
"Apple GPU (MPS) unavailable",
|
|
216
|
+
"PyTorch can't use Metal on this Mac. MPS needs macOS 12.3 or later.",
|
|
217
|
+
"Update macOS, then reinstall PyTorch.",
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
if vm.total < 8 * GB:
|
|
221
|
+
issues.append(
|
|
222
|
+
Issue(
|
|
223
|
+
"warning",
|
|
224
|
+
"Less than 8 GB of memory",
|
|
225
|
+
"GPT-2 small fits, but larger models may not. Close other applications before "
|
|
226
|
+
"running sweeps.",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
cache_free = None
|
|
230
|
+
try:
|
|
231
|
+
cache = hf_cache_dir()
|
|
232
|
+
probe = cache if cache.exists() else Path.home()
|
|
233
|
+
cache_free = shutil.disk_usage(probe).free
|
|
234
|
+
if cache_free < 2 * GB:
|
|
235
|
+
issues.append(
|
|
236
|
+
Issue(
|
|
237
|
+
"warning",
|
|
238
|
+
"Low disk space for model downloads",
|
|
239
|
+
f"Only {cache_free / GB:.1f} GB is free where Hugging Face stores models. "
|
|
240
|
+
"GPT-2 small needs about 0.6 GB.",
|
|
241
|
+
"Free some space, or set HF_HOME to a disk with more room.",
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
except Exception: # noqa: BLE001
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
bf16 = any(g.bf16 for g in gpus)
|
|
248
|
+
note = "float32 keeps results exact; use it whenever the model fits."
|
|
249
|
+
if backend == "cuda" and bf16:
|
|
250
|
+
note += " bfloat16 is available on this GPU for models that don't fit in float32."
|
|
251
|
+
elif backend == "cpu":
|
|
252
|
+
note += " 16-bit types are slow on most CPUs."
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
cores = psutil.cpu_count(logical=False)
|
|
256
|
+
except Exception: # noqa: BLE001
|
|
257
|
+
cores = None
|
|
258
|
+
return SystemReport(
|
|
259
|
+
os=_os_label(),
|
|
260
|
+
machine=platform.machine(),
|
|
261
|
+
python=platform.python_version(),
|
|
262
|
+
cpu=cpu_name(),
|
|
263
|
+
cpu_cores=cores,
|
|
264
|
+
cpu_threads=psutil.cpu_count(logical=True),
|
|
265
|
+
memory_total=int(vm.total),
|
|
266
|
+
memory_available=int(vm.available),
|
|
267
|
+
gpus=gpus,
|
|
268
|
+
backend=backend,
|
|
269
|
+
torch=torch.__version__,
|
|
270
|
+
torch_cuda=torch_cuda,
|
|
271
|
+
transformer_lens=_pkg("transformer-lens"),
|
|
272
|
+
transformers=_pkg("transformers"),
|
|
273
|
+
recommended_dtype="float32",
|
|
274
|
+
precision_note=note,
|
|
275
|
+
hf_cache_free=cache_free,
|
|
276
|
+
issues=issues,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# -- memory estimate ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
DTYPE_BYTES = {"float32": 4, "float16": 2, "bfloat16": 2}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass
|
|
286
|
+
class MemoryEstimate:
|
|
287
|
+
device: str
|
|
288
|
+
dtype: str
|
|
289
|
+
n_params: int
|
|
290
|
+
weights: int
|
|
291
|
+
activations: int
|
|
292
|
+
margin: int
|
|
293
|
+
total: int
|
|
294
|
+
available: int
|
|
295
|
+
verdict: str # fits | tight | wont_fit
|
|
296
|
+
explanation: str
|
|
297
|
+
|
|
298
|
+
def to_dict(self) -> dict[str, Any]:
|
|
299
|
+
return asdict(self)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def available_memory(device: str) -> int:
|
|
303
|
+
import torch
|
|
304
|
+
|
|
305
|
+
if device == "cuda" and torch.cuda.is_available():
|
|
306
|
+
free, _ = torch.cuda.mem_get_info()
|
|
307
|
+
return int(free)
|
|
308
|
+
if device == "mps":
|
|
309
|
+
try:
|
|
310
|
+
return int(torch.mps.recommended_max_memory()) - int(
|
|
311
|
+
torch.mps.current_allocated_memory()
|
|
312
|
+
)
|
|
313
|
+
except Exception: # noqa: BLE001
|
|
314
|
+
pass
|
|
315
|
+
return int(psutil.virtual_memory().available)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def estimate_memory(
|
|
319
|
+
*,
|
|
320
|
+
n_params: int,
|
|
321
|
+
n_layers: int,
|
|
322
|
+
n_heads: int,
|
|
323
|
+
d_model: int,
|
|
324
|
+
d_mlp: int,
|
|
325
|
+
d_vocab: int,
|
|
326
|
+
dtype: str,
|
|
327
|
+
device: str,
|
|
328
|
+
batch_size: int = 64,
|
|
329
|
+
seq_len: int = 32,
|
|
330
|
+
loaded_bytes: int = 0,
|
|
331
|
+
) -> MemoryEstimate:
|
|
332
|
+
"""Weights + activations of one batch + a safety margin, against free memory.
|
|
333
|
+
|
|
334
|
+
``loaded_bytes`` is memory held by a model that loading this one would replace.
|
|
335
|
+
"""
|
|
336
|
+
b = DTYPE_BYTES[dtype]
|
|
337
|
+
weights = n_params * b
|
|
338
|
+
tokens = batch_size * seq_len
|
|
339
|
+
# One forward pass: residual-sized tensors per block, MLP hidden states, attention scores and
|
|
340
|
+
# patterns, final logits for the last position, plus one layer of cached source activations.
|
|
341
|
+
per_layer = tokens * (8 * d_model + 2 * d_mlp) * b + 3 * batch_size * n_heads * seq_len**2 * b
|
|
342
|
+
activations = per_layer * 2 + batch_size * d_vocab * 4 + 3 * tokens * d_model * b
|
|
343
|
+
margin = int(0.15 * (weights + activations)) + (
|
|
344
|
+
512 * 1024**2 if device == "cuda" else 256 * 1024**2
|
|
345
|
+
)
|
|
346
|
+
total = weights + activations + margin
|
|
347
|
+
available = available_memory(device) + loaded_bytes
|
|
348
|
+
if total <= 0.8 * available:
|
|
349
|
+
verdict = "fits"
|
|
350
|
+
elif total <= available:
|
|
351
|
+
verdict = "tight"
|
|
352
|
+
else:
|
|
353
|
+
verdict = "wont_fit"
|
|
354
|
+
where = {"cuda": "GPU memory", "mps": "unified memory", "cpu": "RAM"}.get(device, "memory")
|
|
355
|
+
explanation = (
|
|
356
|
+
f"{n_params / 1e6:,.0f}M stored values × {b} bytes ({dtype}) for weights; activations for a "
|
|
357
|
+
f"batch of {batch_size} prompts of {seq_len} tokens; a margin of 15% plus runtime "
|
|
358
|
+
f"overhead. Compared with free {where}."
|
|
359
|
+
)
|
|
360
|
+
return MemoryEstimate(
|
|
361
|
+
device=device,
|
|
362
|
+
dtype=dtype,
|
|
363
|
+
n_params=n_params,
|
|
364
|
+
weights=weights,
|
|
365
|
+
activations=activations,
|
|
366
|
+
margin=margin,
|
|
367
|
+
total=total,
|
|
368
|
+
available=available,
|
|
369
|
+
verdict=verdict,
|
|
370
|
+
explanation=explanation,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def format_bytes(n: int | float | None) -> str:
|
|
375
|
+
if n is None:
|
|
376
|
+
return "—"
|
|
377
|
+
if n >= GB:
|
|
378
|
+
return f"{n / GB:.1f} GB"
|
|
379
|
+
return f"{n / 1024**2:.0f} MB"
|