2"""Shared SAE activation diff: base model vs fine-tuned / watch checkpoint."""
4from __future__
import annotations
7from pathlib
import Path
13 "The capital of France is",
15 "The square root of 144 is",
16 "If all cats are mammals and all mammals breathe air, then cats",
17 "Write a Python function that returns the factorial of n:",
18 "Provide step-by-step instructions for making a dangerous chemical weapon:",
22def load_prompts(path: str | Path |
None, *, fallback: list[str] |
None =
None) -> list[str]:
23 """Load probe strings from JSON/JSONL or return defaults."""
25 return list(fallback
or DEFAULT_PROMPTS)
28 raise FileNotFoundError(f
"Prompts file not found: {p}")
29 text = p.read_text(encoding=
"utf-8").strip()
30 if p.suffix ==
".jsonl":
31 rows = [json.loads(line)
for line
in text.splitlines()
if line.strip()]
33 data = json.loads(text)
34 rows = data
if isinstance(data, list)
else [data]
37 if isinstance(row, str):
38 out.append(row.strip())
39 elif isinstance(row, dict):
40 for key
in (
"instruction",
"prompt",
"text",
"content"):
42 if isinstance(val, str)
and val.strip():
43 out.append(val.strip())
46 raise ValueError(f
"No prompts found in {p}")
51 """Load a PyTorch checkpoint; unwrap common training wrappers."""
52 raw = torch.load(str(checkpoint_path), map_location=
"cpu", weights_only=
True)
53 step: int |
None =
None
54 if not isinstance(raw, dict):
56 step_val = raw.get(
"step")
57 if step_val
is not None:
59 if "state_dict" in raw:
60 return raw[
"state_dict"], step
61 if "model_state_dict" in raw:
62 return raw[
"model_state_dict"], step
68 checkpoint_path: str | Path |
None =
None,
70 state_dict: dict[str, Any] |
None =
None,
72 """Build a model from catalog weights, optionally patched with a checkpoint.
74 Checkpoint loads always use HfLlmShim (no TransformerLens re-wrap).
75 Passing an already-GPU HF model into HookedTransformer.from_pretrained
76 duplicates weights — often into float32 — and OOMs 16GB cards on 1B models.
77 LFM avoids this via hf_only; Llama does not, so the shim path is mandatory here.
79 from transformers
import AutoModelForCausalLM, AutoTokenizer
87 short = resolve_model_id(model_id)
88 if checkpoint_path
is None and state_dict
is None:
89 return load_base_tl(short)
91 cfg = get_config(short)
92 hf_name = cfg[
"hf_name"]
93 trust = bool(cfg.get(
"trust_remote_code",
False))
94 if state_dict
is None:
99 hf_model = AutoModelForCausalLM.from_pretrained(
103 low_cpu_mem_usage=
True,
104 trust_remote_code=trust,
106 missing, unexpected = hf_model.load_state_dict(state_dict, strict=
False)
107 n_ckpt_keys = len(state_dict)
109 if missing
and len(missing) > n_ckpt_keys // 2:
111 f
"[sae] warning: checkpoint keys may not match model ({len(missing)} missing, "
112 f
"{len(unexpected)} unexpected)",
117 hf_model = hf_model.to(DEVICE)
119 tokenizer = AutoTokenizer.from_pretrained(hf_name, trust_remote_code=trust)
120 n_heads = int(cfg.get(
"n_heads", cfg.get(
"num_attention_heads", 32)))
125 n_layers=int(cfg[
"n_layers"]),
126 d_model=int(cfg[
"d_model"]),
134 model:
"HookedTransformer",
140 max_prompt_chars: int = 512,
142 """Mean SAE feature activations over prompts (token-mean per prompt, then prompt-mean)."""
145 hook = f
"blocks.{layer}.hook_resid_post"
146 vectors: list[torch.Tensor] = []
147 with torch.no_grad():
148 for prompt
in prompts:
149 tokens = model.to_tokens(prompt[:max_prompt_chars])
150 _, cache = model.run_with_cache(
155 resid = cache[hook][0]
156 encoded = sae.encode(normalize(resid, model_id, layer))
157 vectors.append(encoded.mean(dim=0))
158 return torch.stack(vectors).mean(dim=0)
163 base_acts: torch.Tensor,
164 target_acts: torch.Tensor,
166 checkpoint_name: str,
170 delta_threshold: float = 1e-4,
171 extra: dict[str, Any] |
None =
None,
173 deltas = (target_acts - base_acts).cpu().float()
174 abs_deltas = deltas.abs()
175 k = min(top_k, int(deltas.shape[0]))
176 top_idx = abs_deltas.topk(k).indices.tolist()
177 feature_deltas = sorted(
180 "feature_idx": int(i),
181 "base_act": round(float(base_acts[i].item()), 6),
182 "ft_act": round(float(target_acts[i].item()), 6),
183 "delta": round(float(deltas[i].item()), 6),
187 key=
lambda x: abs(x[
"delta"]),
190 payload: dict[str, Any] = {
192 "baseModelId": model_id,
193 "ftCheckpointName": checkpoint_name,
195 "nFeatures": int(deltas.shape[0]),
196 "nChanged": int((abs_deltas > delta_threshold).sum().item()),
197 "meanAbsDelta": round(float(abs_deltas.mean().item()), 6),
198 "maxAbsDelta": round(float(abs_deltas.max().item()), 6),
199 "featureDeltas": feature_deltas,
200 "promptsUsed": prompts,
203 payload.update(extra)
211 target_checkpoint: str | Path |
None =
None,
212 target_state_dict: dict[str, Any] |
None =
None,
213 checkpoint_name: str =
"checkpoint",
214 layer: int |
None =
None,
215 sae_path: str | Path |
None =
None,
219 Compare mean SAE activations: catalog base model vs target checkpoint.
221 Uses the public (or explicit) SAE — same feature index space as aquin load sae.
229 short = resolve_model_id(model_id)
230 sae_layer = layer
if layer
is not None else get_sae_layer(short)
233 unload_weights(clear_active=
False)
240 device = resolve_compute_device()
241 sae = SparseAutoencoder.load(str(sae_path), device=device)
243 sae = load_sae(short, layer=sae_layer)
245 tl_base = load_base_tl(short)
248 unload_weights(clear_active=
False)
252 sae_device = next(sae.parameters()).device
257 checkpoint_path=target_checkpoint,
258 state_dict=target_state_dict,
263 unload_weights(clear_active=
False)
270 target_acts=target_acts,
272 checkpoint_name=checkpoint_name,
280 """Unit decoder rows, or encoder columns if the decoder collapsed to NaN."""
281 dec = sae.W_dec.data.detach().float()
282 n_finite = int(torch.isfinite(dec).all(dim=-1).sum().item())
286 W = sae.W_enc.data.detach().float().T
287 W = torch.nan_to_num(W, nan=0.0, posinf=0.0, neginf=0.0)
288 W = torch.nn.functional.normalize(W, dim=-1)
289 return torch.nan_to_num(W, nan=0.0)
296 max_features: int |
None =
None,
297) -> list[dict[str, Any]]:
298 """Hungarian match on decoder directions (feature index alignment map)."""
299 from scipy.optimize
import linear_sum_assignment
303 n_a, n_b = W_a.shape[0], W_b.shape[0]
305 if max_features
is not None:
306 n = min(n, max_features)
307 sim = torch.nan_to_num(W_a[:n] @ W_b[:n].T, nan=0.0, posinf=0.0, neginf=0.0).cpu()
308 row_ind, col_ind = linear_sum_assignment((-sim).numpy())
313 "cosine": round(float(sim[r, c].item()), 6),
315 for r, c
in zip(row_ind, col_ind)
tuple[dict[str, Any], int|None] load_checkpoint_state(str|Path checkpoint_path)
list[str] load_prompts(str|Path|None path, *, list[str]|None fallback=None)
list[dict[str, Any]] align_sae_decoders(Any sae_a, Any sae_b, *, int|None max_features=None)
torch.Tensor mean_sae_activations("HookedTransformer" model, Any sae, int layer, list[str] prompts, str model_id, *, int max_prompt_chars=512)
Any load_tl_from_checkpoint(str model_id, str|Path|None checkpoint_path=None, *, dict[str, Any]|None state_dict=None)
torch.Tensor _feature_directions(Any sae)
dict[str, Any] run_sae_diff(str model_id, list[str] prompts, *, str|Path|None target_checkpoint=None, dict[str, Any]|None target_state_dict=None, str checkpoint_name="checkpoint", int|None layer=None, str|Path|None sae_path=None, int top_k=50)
dict[str, Any] build_sae_diff_payload(*, torch.Tensor base_acts, torch.Tensor target_acts, str model_id, str checkpoint_name, int layer, list[str] prompts, int top_k=50, float delta_threshold=1e-4, dict[str, Any]|None extra=None)