6"""Multi-layer SAE statistics export for LLMs."""
8from __future__
import annotations
12from pathlib
import Path
19DEVICE = resolve_compute_device()
21_PROBE_TEXT_KEYS = (
"text",
"prompt",
"input",
"query",
"instruction")
22_META_KEYS = (
"id",
"stressor",
"lang",
"quant_run_id",
"condition",
"label",
"group")
26 return Path.home() /
".aquin" /
"sae" / model_id
30 if not layers
or layers.strip().lower() ==
"all":
32 wanted: list[int] = []
33 for part
in layers.split(
","):
38 wanted.append(int(part))
39 except ValueError
as exc:
40 raise ValueError(f
"Invalid --layers value {part!r} (expected integers like 8 or 8,9,10,11)")
from exc
42 raise ValueError(
"--layers must list at least one layer index, e.g. --layers 11 or --layers 8,9,10,11")
47 return "\n".join(f
" aquin load sae {model_id}-l{layer}" for layer
in layers)
55 layers_arg: str |
None,
57 """Actionable error when SAE checkpoint files are missing on disk."""
63 lines.append(f
"No LLM SAE checkpoints found for {model_id}.")
64 lines.append(f
"Expected directory: {sae_dir}/")
65 lines.append(
"Expected files like: sae_layer<N>.pt")
67 lines.append(f
"Some requested LLM SAE layers are not downloaded for {model_id}.")
68 lines.append(f
"Downloaded layers: {available}")
71 missing = [layer
for layer
in wanted
if layer
not in available]
73 lines.append(f
"Missing layer(s): {missing}")
75 lines.append(
"No --layers specified; need at least one downloaded checkpoint.")
78 lines.append(
"Pull checkpoints first (or use aquin load sae --path):")
80 [layer
for layer
in (wanted
or [])
if layer
not in available]
84 if not pull_layers
and wanted:
89 lines.append(
"Load an SAE: aquin load sae <model-l{n}> or --path <file.pt>")
90 return "\n".join(lines)
94 return Path(os.path.expanduser(path)).resolve()
98 probes: list[dict[str, Any]] = []
99 for i, row
in enumerate(rows):
100 if isinstance(row, str):
101 probes.append({
"id": f
"probe_{i}",
"text": row})
103 if not isinstance(row, dict):
104 raise ValueError(f
"Probe row {i} must be string or object")
105 text = next((str(row[k])
for k
in _PROBE_TEXT_KEYS
if row.get(k)),
None)
107 raise ValueError(f
"Probe row {i} missing text field ({', '.join(_PROBE_TEXT_KEYS)})")
108 out: dict[str, Any] = {
"id": row.get(
"id")
or f
"probe_{i}",
"text": text}
109 for key
in _META_KEYS:
110 if row.get(key)
is not None:
114 raise ValueError(
"No probes found")
118def load_probes(path: str | list[Any]) -> list[dict[str, Any]]:
119 """Load probe rows from a path, inline JSON, or an in-memory list."""
120 if isinstance(path, list):
122 raw = str(path).strip()
123 if raw.startswith(
"[")
or raw.startswith(
"{"):
124 data = json.loads(raw)
125 if isinstance(data, list):
127 if isinstance(data, dict)
and isinstance(data.get(
"probes"), list):
129 raise ValueError(
"Expected a JSON array of probe strings")
133 resolved = resolve_prompts_path(
_expand(raw))
135 if "/" not in raw
and "\\" not in raw
and not raw.lower().endswith((
".json",
".jsonl")):
137 raise FileNotFoundError(
138 f
"Probes file not found: {path} "
139 f
"(tried cwd and repo parent)"
143 if p.suffix.lower() ==
".jsonl":
145 with p.open(encoding=
"utf-8")
as f:
149 rows.append(json.loads(line))
151 with p.open(encoding=
"utf-8")
as f:
152 loaded = json.load(f)
153 if isinstance(loaded, list):
155 elif isinstance(loaded, dict)
and isinstance(loaded.get(
"probes"), list):
156 rows = loaded[
"probes"]
158 raise ValueError(f
"Expected JSON array or {{probes: [...]}} in {p}")
164 available: list[int],
166 model_id: str |
None =
None,
169 if layers
is not None and not isinstance(layers, str):
171 if not layers
or layers.strip().lower() ==
"all":
176 else "No SAE layers available on disk."
178 return list(available)
181 return list(available)
182 missing = [layer
for layer
in wanted
if layer
not in available]
189 f
"No SAE checkpoint for layer(s) {missing}. Downloaded: {available or 'none'}"
197 return get_available_sae_layers(resolve_model_id(model_id))
200def _feature_stats(mean_acts: torch.Tensor, *, top_k: int) -> dict[str, Any]:
201 """mean_acts: (n_features,)"""
202 mean_acts = torch.nan_to_num(mean_acts, nan=0.0, posinf=0.0, neginf=0.0)
203 n_features = int(mean_acts.shape[0])
204 active = mean_acts > 1e-6
205 l0 = float(active.sum().item())
206 sparsity = round(1.0 - l0 / max(n_features, 1), 6)
207 top = mean_acts.topk(min(top_k, n_features))
209 {
"feature_idx": int(i),
"mean_activation": round(float(v), 6)}
210 for i, v
in zip(top.indices.tolist(), top.values.tolist())
213 "n_features": n_features,
214 "mean_l0": round(l0, 4),
215 "sparsity": sparsity,
216 "mean_activation": round(float(mean_acts.mean().item()), 6),
217 "max_activation": round(float(mean_acts.max().item()), 6),
218 "top_features": top_features,
231 tokens = model.to_tokens(text[:512])
232 hook = f
"blocks.{layer}.hook_resid_post"
233 with torch.no_grad():
234 _, cache = model.run_with_cache(tokens, names_filter=hook, return_type=
None)
235 resid = cache[hook][0]
236 encoded = sae.encode(normalize(resid, model_id, layer))
237 return torch.nan_to_num(encoded.mean(dim=0).cpu(), nan=0.0, posinf=0.0, neginf=0.0)
243 probes: list[dict[str, Any]],
250 layer_stats: list[dict[str, Any]] = []
251 heatmap_rows: list[str] = []
252 heatmap_cols = [f
"L{layer}" for layer
in layers]
253 heatmap_values: list[list[float]] = []
257 sae = load_sae(model_id, layer)
258 except Exception
as exc:
261 "sae_available":
False,
266 per_probe: list[dict[str, Any]] = []
267 accum: list[torch.Tensor] = []
271 l0 = float((acts > 1e-6).sum().item())
273 "probe_id": probe[
"id"],
274 **{k: probe[k]
for k
in _META_KEYS
if k
in probe},
275 "mean_l0": round(l0, 4),
276 "mean_activation": round(float(acts.mean().item()), 6),
279 mean_acts = torch.stack(accum).mean(dim=0)
282 "sae_available":
True,
284 "per_probe": per_probe,
288 heatmap_rows.append(str(probe[
"id"]))
289 row_vals: list[float] = []
291 layer_entry = next((x
for x
in layer_stats
if x.get(
"layer") == layer),
None)
292 if not layer_entry
or not layer_entry.get(
"sae_available"):
296 (p
for p
in layer_entry.get(
"per_probe", [])
if p.get(
"probe_id") == probe[
"id"]),
299 row_vals.append(float(match[
"mean_l0"])
if match
else 0.0)
300 heatmap_values.append(row_vals)
307 layer_stats=layer_stats,
308 heatmap_rows=heatmap_rows,
309 heatmap_cols=heatmap_cols,
310 heatmap_values=heatmap_values,
319 probes: list[dict[str, Any]],
321 layer_stats: list[dict[str, Any]],
322 heatmap_rows: list[str],
323 heatmap_cols: list[str],
324 heatmap_values: list[list[float]],
329 "layer": entry[
"layer"],
330 "mean_l0": entry.get(
"mean_l0"),
331 "sparsity": entry.get(
"sparsity"),
332 "mean_activation": entry.get(
"mean_activation"),
333 "sae_available": entry.get(
"sae_available",
False),
335 for entry
in layer_stats
336 if entry.get(
"sae_available")
341 "model_id": model_id,
343 "n_probes": len(probes),
344 "layers_requested": layers,
347 "layer_stats": layer_stats,
348 "layer_profile": profile,
351 "rows": heatmap_rows,
352 "cols": heatmap_cols,
353 "values": heatmap_values,
365 model_id = args.get(
"model_id")
or "llama-3.2-1b"
367 model_id = resolve_model_id(model_id)
368 except ValueError
as exc:
369 return {
"error": str(exc)}
371 prompts_path = args.get(
"prompts")
or args.get(
"prompt")
373 probes = [{
"id":
"probe_0",
"text":
"The capital of France is"}]
377 except (OSError, ValueError, json.JSONDecodeError, TypeError)
as exc:
378 return {
"error": str(exc)}
380 top_k = int(args.get(
"top_k")
or 10)
381 layers_arg = args.get(
"layers")
383 model = get_loaded_model()
386 model = load_model(model_id)
387 except Exception
as exc:
388 return {
"error": f
"Could not load model: {exc}"}
394 model_id, mode=
"llm", available=[], layers_arg=layers_arg,
398 layers =
parse_layers_arg(layers_arg, available, model_id=model_id, mode=
"llm")
399 except ValueError
as exc:
400 return {
"error": str(exc)}
list[int] parse_layers_arg(str|None layers, list[int] available, *, str|None model_id=None, str mode="llm")
dict[str, Any] run_llm_sae_stats(Any model, str model_id, list[dict[str, Any]] probes, *, list[int] layers, int top_k=10)
dict[str, Any] _build_payload(*, str model_id, str mode, list[dict[str, Any]] probes, list[int] layers, list[dict[str, Any]] layer_stats, list[str] heatmap_rows, list[str] heatmap_cols, list[list[float]] heatmap_values, int top_k)
list[dict[str, Any]] load_probes(str|list[Any] path)
list[int]|None _parse_layers_wanted(str|None layers)
dict[str, Any] run_sae_stats(dict[str, Any] args)
list[dict[str, Any]] _rows_to_probes(list[Any] rows)
dict[str, Any] _feature_stats(torch.Tensor mean_acts, *, int top_k)
str _format_load_sae_commands(str model_id, list[int] layers)
Path _sae_cache_dir(str model_id)
list[int] list_llm_sae_layers(str model_id)
str _missing_sae_error(str model_id, *, str mode, list[int] available, str|None layers_arg)
torch.Tensor _llm_probe_acts(Any model, Any sae, str model_id, int layer, str text)