26 mu_h = honest.mean(dim=0)
27 mu_d = deceptive.mean(dim=0)
29 l2_sep = float(delta.norm().item())
30 scale = float(mu_h.norm().item() + mu_d.norm().item()) + 1e-8
31 signal = l2_sep / scale
33 cos = float(torch.nn.functional.cosine_similarity(mu_h.unsqueeze(0), mu_d.unsqueeze(0)).item())
34 cos_sep = max(0.0, 1.0 - cos)
36 direction =
_unit(delta)
37 proj_h = float((honest @ direction).mean().item())
38 proj_d = float((deceptive @ direction).mean().item())
39 proj_delta = abs(proj_d - proj_h)
42 "signal": round(signal, 6),
43 "l2_sep": round(l2_sep, 6),
44 "cos_sep": round(cos_sep, 6),
45 "proj_delta": round(proj_delta, 6),
49def _status(signal: float) -> str:
50 if signal <= SIGNAL_COLLAPSED:
52 if signal <= SIGNAL_WEAK:
62 peak = max(layers, key=
lambda r: float(r.get(
"signal")
or 0.0))
63 peak_layer = int(peak[
"layer"])
64 peak_signal = float(peak[
"signal"])
66 after = [r
for r
in layers
if int(r[
"layer"]) > peak_layer]
68 worst = min(after, key=
lambda r: float(r.get(
"signal")
or 0.0))
69 collapse_layer = int(worst[
"layer"])
70 drop = round(peak_signal - float(worst[
"signal"]), 6)
72 worst = min(layers, key=
lambda r: float(r.get(
"signal")
or 0.0))
73 collapse_layer = int(worst[
"layer"])
74 drop = round(peak_signal - float(worst[
"signal"]), 6)
77 "peak_layer": peak_layer,
78 "collapse_layer": collapse_layer,
79 "drop_from_peak": drop,
80 "peak_signal": round(peak_signal, 6),
81 "collapse_signal": round(float(worst[
"signal"]), 6),
86 honest_by_layer: dict[int, torch.Tensor],
87 deceptive_by_layer: dict[int, torch.Tensor],
88) -> list[dict[str, Any]]:
89 layers_out: list[dict[str, Any]] = []
90 for layer
in sorted(set(honest_by_layer) & set(deceptive_by_layer)):
91 metrics =
_centroid_signal(honest_by_layer[layer], deceptive_by_layer[layer])
95 "status":
_status(metrics[
"signal"]),
96 "n_honest": int(honest_by_layer[layer].shape[0]),
97 "n_deceptive": int(deceptive_by_layer[layer].shape[0]),
103 baseline_layers: list[dict[str, Any]],
104 stressor_layers: list[dict[str, Any]],
105) -> list[dict[str, Any]]:
129) -> dict[str, float]:
130 d =
_unit(direction.float().cpu())
131 if honest.shape[-1] != d.shape[-1]:
133 f
"Direction d_model={d.shape[-1]} does not match activations d_model={honest.shape[-1]}"
135 proj_h = float((honest @ d).mean().item())
136 proj_d = float((deceptive @ d).mean().item())
138 "honest_proj": round(proj_h, 6),
139 "deceptive_proj": round(proj_d, 6),
140 "proj_delta": round(abs(proj_d - proj_h), 6),
148 honest_texts: list[str],
149 deceptive_texts: list[str],
152) -> list[dict[str, Any]] |
None:
153 """Rank SAE features by deceptive−honest mean activation at one layer."""
161 sae = load_sae(model_id, layer)
165 def _mean_feats(texts: list[str]) -> torch.Tensor |
None:
166 feats: list[torch.Tensor] = []
167 hook = f
"blocks.{layer}.hook_resid_post"
169 tokens = model.to_tokens(text)
170 with torch.no_grad():
171 _, cache = model.run_with_cache(tokens, names_filter=
lambda n, h=hook: n == h)
172 if hook
not in cache:
174 act = cache[hook][0, -1].float()
175 with torch.no_grad():
176 f = sae.encode(act.unsqueeze(0))[0].float().cpu()
180 return torch.stack(feats).mean(dim=0)
182 h = _mean_feats(honest_texts)
183 d = _mean_feats(deceptive_texts)
184 if h
is None or d
is None:
186 return _rank_features(h, d, top_k=top_k, direction=
"both")
193 prompts: str | Path |
None =
None,
194 stressor_prompts: str | Path |
None =
None,
195 feature_idx: int |
None =
None,
196 vector_path: str | Path |
None =
None,
197 layer: int |
None =
None,
198 top_k_features: int = 8,
199 collect_layer_activations: Any |
None =
None,
202 Rank layers by honest vs deceptive representation strength.
204 Default: contrastive centroid signal per resid_post layer.
205 Optional stressor prompts → per-layer collapse_delta.
206 Optional feature_idx / vector → direction projection at the relevant layer.
210 collect = collect_layer_activations
or _collect_layer_activations
213 honest, deceptive, meta = load_deception_probes(prompts)
214 except (FileNotFoundError, ValueError)
as e:
215 return {
"error": str(e)}
217 honest = honest[:MAX_PROBES_PER_CLASS]
218 deceptive = deceptive[:MAX_PROBES_PER_CLASS]
220 honest_acts = collect(model, honest)
221 deceptive_acts = collect(model, deceptive)
225 out: dict[str, Any] = {
226 "mode":
"contrastive",
227 "prompts_path": meta.get(
"prompts_path"),
228 "n_honest": len(honest),
229 "n_deceptive": len(deceptive),
232 "n_collapsed": sum(1
for r
in layers
if r[
"status"] ==
"collapsed"),
233 "n_weak": sum(1
for r
in layers
if r[
"status"] ==
"weak"),
236 if stressor_prompts
is not None:
238 s_honest, s_deceptive, s_meta = load_deception_probes(stressor_prompts)
239 except (FileNotFoundError, ValueError)
as e:
240 return {**out,
"error": f
"stressor probes: {e}"}
241 s_honest = s_honest[:MAX_PROBES_PER_CLASS]
242 s_deceptive = s_deceptive[:MAX_PROBES_PER_CLASS]
244 collect(model, s_honest),
245 collect(model, s_deceptive),
249 "prompts_path": s_meta.get(
"prompts_path"),
250 "n_honest": len(s_honest),
251 "n_deceptive": len(s_deceptive),
252 "layers": stress_rows,
255 worst = max(stress_rows, key=
lambda r: float(r[
"collapse_delta"]))
256 out[
"stressor"][
"max_collapse_layer"] = int(worst[
"layer"])
257 out[
"stressor"][
"max_collapse_delta"] = float(worst[
"collapse_delta"])
259 out[
"collapse_layer"] = int(worst[
"layer"])
260 out[
"collapse_signal"] = float(worst[
"stressor_signal"])
261 out[
"drop_from_peak"] = float(worst[
"collapse_delta"])
262 out[
"mode"] =
"contrastive+stressor"
264 direction_info: dict[str, Any] |
None =
None
265 direction_vec: torch.Tensor |
None =
None
266 direction_layer: int |
None = layer
272 payload = load_steer_vector_file(vector_path)
273 direction_vec = steer_vector_tensor(
274 payload, device=
"cpu", dtype=torch.float32,
276 direction_layer = int(payload.get(
"layer", direction_layer
or 0))
279 "vector_path": str(vector_path),
280 "layer": direction_layer,
281 "feature_idx": payload.get(
"feature_idx"),
283 except (OSError, ValueError)
as e:
284 return {**out,
"error": f
"vector: {e}"}
285 elif feature_idx
is not None:
289 direction_vec, direction_layer, _ = resolve_feature_direction(
290 model_id, int(feature_idx), layer=layer,
292 direction_vec = direction_vec.float().cpu()
295 "feature_idx": int(feature_idx),
296 "layer": int(direction_layer),
298 except Exception
as e:
299 return {**out,
"error": f
"feature direction: {e}"}
301 if direction_vec
is not None and direction_layer
is not None:
302 h = honest_acts.get(int(direction_layer))
303 d = deceptive_acts.get(int(direction_layer))
304 if h
is not None and d
is not None:
307 direction_info = {**(direction_info
or {}), **proj}
308 except ValueError
as e:
309 return {**out,
"error": str(e)}
310 out[
"direction"] = direction_info
312 collapse_l = out.get(
"collapse_layer")
313 if collapse_l
is not None:
320 top_k=int(top_k_features),
322 if feats
is not None:
323 out[
"features_at_collapse"] = feats
324 out[
"features_layer"] = int(collapse_l)
dict[str, Any] run_localize_collapse(Any model, str model_id, *, str|Path|None prompts=None, str|Path|None stressor_prompts=None, int|None feature_idx=None, str|Path|None vector_path=None, int|None layer=None, int top_k_features=8, Any|None collect_layer_activations=None)