AQIT 0.1.0
Loading...
Searching...
No Matches
sae_catalog_metrics.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""Pre-publish metrics for public SAE catalog rows (UMAP + layer InterpScore)."""
3
4from __future__ import annotations
5
6import json
7from pathlib import Path
8from typing import Any
9
10import torch
11
12from aquin.compute.sae import SparseAutoencoder
13
14CATALOG_PROBE_PROMPTS = [
15 "The capital of France is",
16 "In quantum mechanics, particles can",
17 "The recipe calls for two cups of",
18 "She opened the letter and read",
19 "function merge_sort(arr):",
20]
21
22
23def compute_umap_points_from_sae(sae_path: str | Path) -> list[dict[str, Any]]:
24 """3D UMAP of decoder rows (same recipe as bridge._compute_umap_points)."""
25 sae = SparseAutoencoder.load(str(sae_path), device="cpu")
26 w = sae.W_dec.float().cpu().detach()
27 try:
28 import umap
29
30 reducer = umap.UMAP(n_components=3, random_state=42, n_neighbors=15)
31 embedding = reducer.fit_transform(w.numpy())
32 return [
33 {"feature_idx": i, "x": float(r[0]), "y": float(r[1]), "z": float(r[2])}
34 for i, r in enumerate(embedding)
35 ]
36 except ImportError:
37 w_centered = w - w.mean(0)
38 _, _, vt = torch.linalg.svd(w_centered, full_matrices=False)
39 proj = (w_centered @ vt[:3].T).numpy()
40 return [
41 {"feature_idx": i, "x": float(r[0]), "y": float(r[1]), "z": float(r[2])}
42 for i, r in enumerate(proj)
43 ]
44
45
47 *,
48 model_id: str,
49 layer: int,
50 prompts: list[str],
51 n_per_prompt: int,
52 max_features: int,
53) -> list[tuple[int, float, str]]:
54 from aquin.compute.causal_trace import run_chat
55 from aquin.compute.feature_analysis import run_feature_analysis_unlabeled
56 from aquin.compute.model_loader import get_loaded_model, load_model
57
58 model = get_loaded_model()
59 if model is None:
60 model = load_model(model_id)
61
62 best: dict[int, tuple[float, str]] = {}
63 for prompt in prompts:
64 try:
65 response = run_chat(prompt, model_id=model_id, max_new_tokens=120, temperature=0.7)
66 except Exception as exc:
67 print(f"[catalog-metrics] generation failed ({prompt[:40]!r}): {exc}", flush=True)
68 continue
69 try:
70 feat = run_feature_analysis_unlabeled(
71 prompt, response, model, model_id=model_id, layer=layer,
72 )
73 except Exception as exc:
74 print(f"[catalog-metrics] feature pass failed ({prompt[:40]!r}): {exc}", flush=True)
75 continue
76 for row in (feat.get("top_response_features") or [])[:n_per_prompt]:
77 idx = int(row["feature_idx"])
78 act = float(row.get("activation") or 0.0)
79 prev = best.get(idx)
80 if prev is None or act > prev[0]:
81 best[idx] = (act, prompt)
82
83 ranked = sorted(
84 ((idx, act, prompt) for idx, (act, prompt) in best.items()),
85 key=lambda t: t[1],
86 reverse=True,
87 )
88 return ranked[:max_features]
89
90
92 *,
93 model_id: str,
94 layer: int,
95 sae_path: str | Path,
96 prompts: list[str] | None = None,
97 n_features: int = 8,
98 n_per_prompt: int = 5,
99 n_samples: int = 8,
100 client: Any | None = None,
101) -> dict[str, Any]:
102 """Mean InterpScore over top response features from catalog probe prompts."""
103 from aquin.compute.interp_score import run_interp_score
104 from aquin.compute.model_loader import get_loaded_model, load_model
105 from aquin.compute.openai_client import get_openai_client
106 from aquin.compute.user_sae import activate_user_sae
107
108 sae_path = Path(sae_path).resolve()
109 if not sae_path.is_file():
110 raise FileNotFoundError(f"SAE not found: {sae_path}")
111
112 activate_user_sae(model_id=model_id, layer=layer, path=sae_path, name=sae_path.parent.name)
113
114 if client is None:
115 client = get_openai_client({})
116 if client is None:
117 return {
118 "interp_score": None,
119 "feature_scores": [],
120 "n_benchmarked": 0,
121 "error": "OpenAI not configured. Set OPENAI_API_KEY.",
122 }
123
124 probe_prompts = list(prompts or CATALOG_PROBE_PROMPTS)
125 candidates = _collect_top_features(
126 model_id=model_id,
127 layer=layer,
128 prompts=probe_prompts,
129 n_per_prompt=n_per_prompt,
130 max_features=n_features,
131 )
132 if not candidates:
133 return {
134 "interp_score": None,
135 "feature_scores": [],
136 "n_benchmarked": 0,
137 "error": "No active features found on probe prompts.",
138 }
139
140 model = get_loaded_model()
141 if model is None:
142 model = load_model(model_id)
143
144 from aquin.compute.feature_analysis import load_sae
145
146 sae = load_sae(model_id, layer)
147 feature_scores: list[dict[str, Any]] = []
148 scores: list[float] = []
149
150 for feature_idx, activation, prompt in candidates:
151 print(f"[catalog-metrics] InterpScore f{feature_idx} (act={activation:.3f})", flush=True)
152 interp = run_interp_score(
153 feature_idx,
154 prompt,
155 model,
156 sae,
157 client,
158 n_samples=n_samples,
159 model_id=model_id,
160 layer=layer,
161 )
162 score = interp.get("score")
163 row = {
164 "feature_idx": feature_idx,
165 "activation": round(activation, 4),
166 "prompt": prompt,
167 "score": score,
168 "purity_score": interp.get("purity_score"),
169 "label": interp.get("label"),
170 "error": interp.get("error"),
171 }
172 feature_scores.append(row)
173 if score is not None:
174 scores.append(float(score))
175
176 mean_score = round(sum(scores) / len(scores), 4) if scores else None
177 return {
178 "interp_score": mean_score,
179 "feature_scores": feature_scores,
180 "n_benchmarked": len(scores),
181 "n_candidates": len(candidates),
182 "prompts": probe_prompts,
183 }
184
185
187 *,
188 sae_path: str | Path,
189 umap_points: list[dict[str, Any]],
190 metrics: dict[str, Any],
191) -> tuple[Path, Path]:
192 sae_path = Path(sae_path).resolve()
193 umap_path = sae_path.with_suffix(".umap.json")
194 metrics_path = sae_path.with_suffix(".metrics.json")
195 umap_path.write_text(json.dumps({"points": umap_points}, indent=2), encoding="utf-8")
196 metrics_path.write_text(json.dumps(metrics, indent=2), encoding="utf-8")
197 return umap_path, metrics_path
198
199
201 *,
202 model_id: str,
203 layer: int,
204 sae_path: str | Path,
205 skip_interp: bool = False,
206 **interp_kwargs: Any,
207) -> dict[str, Any]:
208 sae_path = Path(sae_path).resolve()
209 print(f"[catalog-metrics] UMAP on {sae_path.name}…", flush=True)
210 umap_points = compute_umap_points_from_sae(sae_path)
211 print(f"[catalog-metrics] UMAP done ({len(umap_points)} points)", flush=True)
212
213 metrics: dict[str, Any] = {"layer": layer, "model_id": model_id, "n_umap_points": len(umap_points)}
214 if not skip_interp:
215 from aquin.compute.loader_shim import apply as shim_apply
216
217 shim_apply()
218 print(f"[catalog-metrics] InterpScore layer {layer}…", flush=True)
220 model_id=model_id,
221 layer=layer,
222 sae_path=sae_path,
223 **interp_kwargs,
224 )
225 metrics.update(interp)
226 if interp.get("interp_score") is not None:
227 print(f"[catalog-metrics] layer {layer} interp_score={interp['interp_score']}", flush=True)
228 elif interp.get("error"):
229 print(f"[catalog-metrics] interp skipped: {interp['error']}", flush=True)
230
231 umap_path, metrics_path = write_catalog_sidecars(
232 sae_path=sae_path, umap_points=umap_points, metrics=metrics,
233 )
234 print(f"[catalog-metrics] wrote {umap_path.name}, {metrics_path.name}", flush=True)
235 return {
236 "umap_path": str(umap_path),
237 "metrics_path": str(metrics_path),
238 "interp_score": metrics.get("interp_score"),
239 "n_umap_points": len(umap_points),
240 }
tuple[Path, Path] write_catalog_sidecars(*, str|Path sae_path, list[dict[str, Any]] umap_points, dict[str, Any] metrics)
list[dict[str, Any]] compute_umap_points_from_sae(str|Path sae_path)
dict[str, Any] compute_and_write_catalog_sidecars(*, str model_id, int layer, str|Path sae_path, bool skip_interp=False, **Any interp_kwargs)
list[tuple[int, float, str]] _collect_top_features(*, str model_id, int layer, list[str] prompts, int n_per_prompt, int max_features)
dict[str, Any] compute_layer_interp_score(*, str model_id, int layer, str|Path sae_path, list[str]|None prompts=None, int n_features=8, int n_per_prompt=5, int n_samples=8, Any|None client=None)