3"""Replay/query saved activation captures by metadata."""
5from __future__
import annotations
8from collections
import defaultdict
9from pathlib
import Path
16 data = json.loads(path.read_text(encoding=
"utf-8"))
17 if not isinstance(data, dict):
18 raise ValueError(f
"Expected JSON object in {path}")
23 root = Path(path).expanduser().resolve()
26 manifest_path = root /
"manifest.json"
27 metadata_path = root /
"metadata.json"
28 summary_path = root /
"summary.jsonl"
29 if metadata_path.is_file():
31 if manifest_path.is_file():
33 files = man.get(
"files")
if isinstance(man.get(
"files"), dict)
else {}
34 meta_files = meta.get(
"files")
if isinstance(meta.get(
"files"), dict)
else {}
35 if files
or meta_files:
36 merged = {**files, **meta_files}
37 if isinstance(files.get(
"activations"), dict)
and isinstance(meta_files.get(
"activations"), dict):
38 merged[
"activations"] = {**files[
"activations"], **meta_files[
"activations"]}
39 meta[
"files"] = merged
40 if not meta.get(
"probes")
and man.get(
"probes"):
41 meta[
"probes"] = man[
"probes"]
42 if not meta.get(
"sae_features")
and man.get(
"sae_features"):
43 meta[
"sae_features"] = man[
"sae_features"]
44 elif manifest_path.is_file():
47 raise FileNotFoundError(f
"No metadata.json or manifest.json in {root}")
49 rows: list[dict[str, Any]] = []
50 if summary_path.is_file():
51 for line
in summary_path.read_text(encoding=
"utf-8").splitlines():
55 row = json.loads(line)
56 if isinstance(row, dict):
60 "metadata_path": metadata_path
if metadata_path.is_file()
else manifest_path,
66def _flatten_probe_row(row: dict[str, Any], probe_lookup: dict[str, dict[str, Any]]) -> dict[str, Any]:
67 probe_id = str(row.get(
"probe_id")
or "")
68 probe = probe_lookup.get(probe_id, {})
69 probe_meta = probe.get(
"metadata")
if isinstance(probe.get(
"metadata"), dict)
else {}
70 row_meta = row.get(
"metadata")
if isinstance(row.get(
"metadata"), dict)
else {}
71 merged_meta = {**probe_meta, **row_meta}
72 flat: dict[str, Any] = {
74 "layer": row.get(
"layer"),
75 "d_model": row.get(
"d_model"),
76 "text": probe.get(
"text"),
78 for key, val
in merged_meta.items():
86 parts: list[tuple[str, str]] = []
87 for chunk
in expr.split(
","):
92 raise ValueError(f
"Bad filter {chunk!r}; use key=value,key2=value2")
93 key, value = chunk.split(
"=", 1)
94 parts.append((key.strip(), value.strip()))
98def _matches(row: dict[str, Any], filters: list[tuple[str, str]]) -> bool:
99 for key, expected
in filters:
103 if str(got) != expected:
111 filter_expr: str |
None =
None,
112 group_by: str |
None =
None,
113 limit: int |
None =
None,
116 manifest = loaded[
"manifest"]
117 probes = manifest.get(
"probes")
if isinstance(manifest.get(
"probes"), list)
else []
119 str(p.get(
"id")): p
for p
in probes
120 if isinstance(p, dict)
and p.get(
"id")
is not None
124 filtered = [r
for r
in rows
if _matches(r, filters)]
125 if limit
is not None and limit >= 0:
126 filtered = filtered[:limit]
128 result: dict[str, Any] = {
129 "root": str(loaded[
"root"]),
130 "metadata_path": str(loaded[
"metadata_path"]),
131 "capture_id": manifest.get(
"capture_id"),
132 "model_id": manifest.get(
"model_id"),
133 "model_mode": manifest.get(
"model_mode"),
135 "n_filtered": len(filtered),
136 "filter": filter_expr,
137 "group_by": group_by,
141 groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
143 groups[str(row.get(group_by,
"unknown"))].append(row)
145 for key, items
in sorted(groups.items(), key=
lambda kv: (-len(kv[1]), kv[0])):
146 layer_counts: dict[str, int] = defaultdict(int)
148 layer_counts[str(item.get(
"layer",
"?"))] += 1
152 "layers": dict(sorted(layer_counts.items(), key=
lambda kv: int(kv[0])
if kv[0].isdigit()
else 999999)),
154 result[
"groups"] = summary
158COHORT_FIELDS = (
"group",
"label",
"lang",
"stressor",
"source")
161def _probe_groups(probes: list[dict[str, Any]], field: str) -> dict[str, list[int]]:
162 grouped: dict[str, list[int]] = defaultdict(list)
163 for i, probe
in enumerate(probes):
164 if not isinstance(probe, dict):
166 meta = probe.get(
"metadata")
if isinstance(probe.get(
"metadata"), dict)
else {}
167 grouped[str(meta.get(field,
"unknown"))].append(i)
172 keys = [k
for k, idxs
in grouped.items()
if idxs]
173 return len(keys) >= 2
177 probes: list[dict[str, Any]],
179) -> tuple[str, dict[str, list[int]], bool]:
180 """Pick a metadata field with ≥2 cohorts. Last resort: split probes in half."""
183 return requested, grouped,
False
184 for field
in COHORT_FIELDS:
185 if field == requested:
189 return field, candidate,
False
193 f
"Need at least 2 groups for {requested!r}; found {len(grouped)}. "
194 "Recapture with labeled probes (metadata group/label/lang)."
197 inferred = {
"a": list(range(0, mid)),
"b": list(range(mid, n))}
198 return requested, inferred,
True
201def _write_json(path: Path, data: dict[str, Any]) ->
None:
202 path.write_text(json.dumps(data, indent=2), encoding=
"utf-8")
208 sae_layer: int |
None =
None,
209 sae: Any |
None =
None,
211 """Encode residual activations in a capture with the SAE for that layer."""
213 manifest = loaded[
"manifest"]
214 root = loaded[
"root"]
215 files = manifest.get(
"files")
if isinstance(manifest.get(
"files"), dict)
else {}
216 sae_rel = files.get(
"sae_features")
217 if sae_rel
and (root / str(sae_rel)).is_file():
221 "sae_features": manifest.get(
"sae_features"),
224 if str(manifest.get(
"granularity")
or "prompt") ==
"token":
225 raise ValueError(
"Cannot encode SAE features on a token-level capture.")
227 model_id = str(manifest.get(
"model_id")
or "").strip()
229 raise ValueError(
"Capture has no model_id; cannot encode SAE features.")
231 activations = files.get(
"activations")
if isinstance(files.get(
"activations"), dict)
else {}
232 enc_layer = int(sae_layer)
if sae_layer
is not None else None
233 if enc_layer
is None:
237 enc_layer = int(get_sae_layer(model_id))
240 if enc_layer
is None or str(enc_layer)
not in activations:
244 for layer
in get_available_sae_layers(model_id):
245 if str(layer)
in activations:
246 enc_layer = int(layer)
250 if enc_layer
is None or str(enc_layer)
not in activations:
252 "Capture has no residual activations for a loadable SAE layer. "
253 "Re-run activations.capture with encode_sae=true (pythia-70m: layer 3)."
256 resid_path = root / str(activations[str(enc_layer)])
257 if not resid_path.is_file():
258 raise FileNotFoundError(f
"Missing residual activations: {resid_path}")
259 resid = torch.load(resid_path, map_location=
"cpu", weights_only=
False)
260 if not isinstance(resid, torch.Tensor)
or resid.dim() != 2:
262 f
"Expected residual matrix [N, d], got {type(resid)} "
263 f
"{tuple(resid.shape) if isinstance(resid, torch.Tensor) else ''}"
269 sae = load_sae(model_id, layer=int(enc_layer))
272 device = next(sae.parameters()).device
273 with torch.no_grad():
274 encoded = sae.encode(normalize(resid.to(device), model_id, int(enc_layer))).float().cpu()
276 sae_rel = f
"sae/sae_layer_{int(enc_layer)}.pt"
277 (root /
"sae").mkdir(parents=
True, exist_ok=
True)
278 torch.save(encoded, root / sae_rel)
280 "layer": int(enc_layer),
281 "n_features": int(encoded.shape[1]),
282 "n_probes": int(encoded.shape[0]),
285 files[
"sae_features"] = sae_rel
286 manifest[
"files"] = files
287 manifest[
"sae_features"] = sae_features
288 manifest[
"encode_sae"] =
True
290 meta_path = loaded[
"metadata_path"]
292 other = root / (
"manifest.json" if meta_path.name ==
"metadata.json" else "metadata.json")
295 other_files = other_data.get(
"files")
if isinstance(other_data.get(
"files"), dict)
else {}
296 other_files[
"sae_features"] = sae_rel
297 other_data[
"files"] = other_files
298 other_data[
"sae_features"] = sae_features
299 other_data[
"encode_sae"] =
True
302 return {
"root": str(root),
"encoded":
True,
"sae_features": sae_features}
310 encode_sae: bool =
True,
311 sae_layer: int |
None =
None,
312 sae: Any |
None =
None,
315 manifest = loaded[
"manifest"]
316 probes = manifest.get(
"probes")
if isinstance(manifest.get(
"probes"), list)
else []
317 files = manifest.get(
"files")
if isinstance(manifest.get(
"files"), dict)
else {}
318 sae_rel = files.get(
"sae_features")
319 sae_path = loaded[
"root"] / str(sae_rel)
if sae_rel
else None
320 if not sae_rel
or sae_path
is None or not sae_path.is_file():
322 raise ValueError(
"Capture has no SAE feature matrix. Re-run capture with --encode-sae.")
325 manifest = loaded[
"manifest"]
326 probes = manifest.get(
"probes")
if isinstance(manifest.get(
"probes"), list)
else []
327 files = manifest.get(
"files")
if isinstance(manifest.get(
"files"), dict)
else {}
328 sae_rel = files.get(
"sae_features")
329 sae_path = loaded[
"root"] / str(sae_rel)
if sae_rel
else None
330 if not sae_rel
or sae_path
is None or not sae_path.is_file():
331 raise ValueError(
"Capture has no SAE feature matrix. Re-run capture with --encode-sae.")
333 feats = torch.load(sae_path, map_location=
"cpu", weights_only=
False)
335 raise ValueError(f
"Expected SAE features [N, F], got shape {tuple(feats.shape)}")
336 if probes
and len(probes) != int(feats.shape[0]):
337 raise ValueError(f
"Probe count {len(probes)} does not match SAE rows {int(feats.shape[0])}")
339 raise ValueError(
"Capture has no probes; features.compare needs a labeled activation capture.")
343 group_means: dict[str, torch.Tensor] = {}
344 for key, idxs
in grouped.items():
347 group_means[key] = feats[torch.tensor(idxs, dtype=torch.long)].mean(dim=0)
349 keys = sorted(group_means)
350 stacked = torch.stack([group_means[k]
for k
in keys], dim=0)
351 spread = stacked.max(dim=0).values - stacked.min(dim=0).values
352 k = min(int(top_k), int(spread.shape[0]))
353 top_idx = spread.topk(k).indices.tolist()
357 per_group = {key: round(float(group_means[key][idx].item()), 6)
for key
in keys}
359 "feature_idx": int(idx),
360 "spread": round(float(spread[idx].item()), 6),
365 "root": str(loaded[
"root"]),
366 "capture_id": manifest.get(
"capture_id"),
367 "model_id": manifest.get(
"model_id"),
369 "n_groups": len(group_means),
370 "group_sizes": {key: len(grouped[key])
for key
in keys},
371 "sae_layer": (manifest.get(
"sae_features")
or {}).get(
"layer"),
372 "n_features": int(feats.shape[1]),
374 "group_inferred": inferred,
dict[str, Any] encode_capture_sae_features(str|Path path, *, int|None sae_layer=None, Any|None sae=None)
bool _matches(dict[str, Any] row, list[tuple[str, str]] filters)
tuple[str, dict[str, list[int]], bool] resolve_compare_group(list[dict[str, Any]] probes, str requested)
dict[str, list[int]] _probe_groups(list[dict[str, Any]] probes, str field)
None _write_json(Path path, dict[str, Any] data)
dict[str, Any] _flatten_probe_row(dict[str, Any] row, dict[str, dict[str, Any]] probe_lookup)
dict[str, Any] load_capture_root(str|Path path)
dict[str, Any] _read_json(Path path)
dict[str, Any] replay_capture(str|Path path, *, str|None filter_expr=None, str|None group_by=None, int|None limit=None)
bool _usable_groups(dict[str, list[int]] grouped)
dict[str, Any] compare_capture_features(str|Path path, *, str group, int top_k=10, bool encode_sae=True, int|None sae_layer=None, Any|None sae=None)
list[tuple[str, str]] _parse_filter(str|None expr)