AQIT 0.1.0
Loading...
Searching...
No Matches
residual_drift.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""Per-layer activation drift: catalog base vs fine-tuned checkpoint (LLM)."""
3
4from __future__ import annotations
5
6from pathlib import Path
7from typing import Any
8
9import torch
10
11from aquin.compute.layer_analysis import _collect_layer_activations
12from aquin.compute.sae_diff import load_checkpoint_state, load_prompts, load_tl_from_checkpoint
13
14
15def _cosine_distance_rows(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
16 """Per-row cosine distance. a, b: (n, d)."""
17 a_n = a.float() / a.float().norm(dim=-1, keepdim=True).clamp(min=1e-8)
18 b_n = b.float() / b.float().norm(dim=-1, keepdim=True).clamp(min=1e-8)
19 return 1.0 - (a_n * b_n).sum(dim=-1)
20
21
23 base_acts: dict[int, torch.Tensor],
24 ft_acts: dict[int, torch.Tensor],
25 n_layers: int,
26) -> list[dict[str, Any]]:
27 rows: list[dict[str, Any]] = []
28 for layer in range(n_layers):
29 base = base_acts.get(layer)
30 ft = ft_acts.get(layer)
31 if base is None or ft is None or base.shape != ft.shape:
32 continue
33 dists = _cosine_distance_rows(base, ft)
34 rows.append({
35 "layer": layer,
36 "mean_cosine_distance": round(float(dists.mean().item()), 6),
37 "max_cosine_distance": round(float(dists.max().item()), 6),
38 "mean_cosine_sim": round(float((1.0 - dists).mean().item()), 6),
39 "n_probes": int(dists.shape[0]),
40 })
41 return rows
42
43
45 base_acts: dict[int, torch.Tensor],
46 ft_acts: dict[int, torch.Tensor],
47 layer_rows: list[dict[str, Any]],
48 probes: list[str],
49) -> list[dict[str, Any]]:
50 if not layer_rows:
51 return []
52 layers = [int(r["layer"]) for r in layer_rows]
53 out: list[dict[str, Any]] = []
54 n_probes = int(layer_rows[0].get("n_probes") or len(probes))
55 for i in range(n_probes):
56 dists: list[float] = []
57 for layer in layers:
58 base = base_acts.get(layer)
59 ft = ft_acts.get(layer)
60 if base is None or ft is None:
61 continue
62 dists.append(float(_cosine_distance_rows(base[i : i + 1], ft[i : i + 1]).item()))
63 if not dists:
64 continue
65 peak_layer = layers[int(max(range(len(dists)), key=lambda j: dists[j]))]
66 preview = probes[i][:80] + ("…" if len(probes[i]) > 80 else "")
67 out.append({
68 "probe_index": i,
69 "probe_preview": preview,
70 "mean_drift": round(sum(dists) / len(dists), 6),
71 "max_drift": round(max(dists), 6),
72 "peak_layer": peak_layer,
73 })
74 return sorted(out, key=lambda r: r["max_drift"], reverse=True)
75
76
78 *,
79 model_id: str,
80 mode: str,
81 activation_mode: str,
82 checkpoint_name: str,
83 checkpoint_path: str,
84 prompts: list[str],
85 layer_profile: list[dict[str, Any]],
86 per_probe: list[dict[str, Any]],
87 step: int | None = None,
88) -> dict[str, Any]:
89 drifts = [float(r["mean_cosine_distance"]) for r in layer_profile]
90 peak = max(layer_profile, key=lambda r: r["mean_cosine_distance"]) if layer_profile else None
91 return {
92 "schema_version": 1,
93 "type": "residualDrift",
94 "baseModelId": model_id,
95 "ftCheckpointName": checkpoint_name,
96 "checkpointPath": checkpoint_path,
97 "modelMode": mode,
98 "activationMode": activation_mode,
99 "trainingStep": step,
100 "nProbes": len(prompts),
101 "nLayers": len(layer_profile),
102 "meanDrift": round(sum(drifts) / len(drifts), 6) if drifts else 0.0,
103 "maxDrift": round(max(drifts), 6) if drifts else 0.0,
104 "peakLayer": peak["layer"] if peak else None,
105 "layerProfile": layer_profile,
106 "topLayers": sorted(layer_profile, key=lambda r: r["mean_cosine_distance"], reverse=True)[:10],
107 "perProbe": per_probe[:25],
108 "promptsUsed": prompts,
109 }
110
111
113 model_id: str,
114 checkpoint_path: str | Path,
115 prompts: list[str],
116 *,
117 checkpoint_name: str = "checkpoint",
118) -> dict[str, Any]:
119 from aquin.compute.causal_trace import load_model as load_base_tl
120 from aquin.compute.device import empty_device_cache
121 from aquin.compute.model_loader import resolve_model_id
122 from aquin.compute.model_runtime import unload_weights
123
124 short = resolve_model_id(model_id)
125 _, step = load_checkpoint_state(checkpoint_path)
126
127 unload_weights(clear_active=False)
128 empty_device_cache()
129
130 tl_base = load_base_tl(short)
131 base_acts = _collect_layer_activations(tl_base, prompts)
132 n_layers = int(tl_base.cfg.n_layers)
133 del tl_base
134 unload_weights(clear_active=False)
135 empty_device_cache()
136
137 tl_ft = load_tl_from_checkpoint(short, checkpoint_path=checkpoint_path)
138 ft_acts = _collect_layer_activations(tl_ft, prompts)
139 del tl_ft
140 unload_weights(clear_active=False)
141 empty_device_cache()
142
143 layer_profile = _layer_drift_rows(base_acts, ft_acts, n_layers)
144 per_probe = _per_probe_rows(base_acts, ft_acts, layer_profile, prompts)
145
146 return _finalize_payload(
147 model_id=short,
148 mode="llm",
149 activation_mode="hook_resid_post_last_token",
150 checkpoint_name=checkpoint_name,
151 checkpoint_path=str(checkpoint_path),
152 prompts=prompts,
153 layer_profile=layer_profile,
154 per_probe=per_probe,
155 step=step,
156 )
157
158
160 model_id: str,
161 checkpoint_path: str | Path,
162 prompts: list[str] | None = None,
163 *,
164 checkpoint_name: str | None = None,
165) -> dict[str, Any]:
166 from aquin.compute.model_loader import resolve_model_id
167
168 ckpt = Path(checkpoint_path)
169 if not ckpt.exists():
170 raise FileNotFoundError(f"Checkpoint not found: {ckpt}")
171
172 probe_list = list(prompts or load_prompts(None))
173 name = checkpoint_name or ckpt.stem
174
175 short = resolve_model_id(model_id)
176 return run_llm_residual_drift(short, ckpt, probe_list, checkpoint_name=name)
177
178
179def run_residual_drift_from_args(args: dict[str, Any]) -> dict[str, Any]:
180 from aquin.compute.model_loader import get_active_model_id, resolve_model_id
181
182 model_id = args.get("model_id") or get_active_model_id() or "llama-3.2-1b"
183 checkpoint = args.get("checkpoint")
184 if not checkpoint:
185 return {"error": "Missing --checkpoint <path>. Run: aquin diff residue --help"}
186
187 try:
188 model_id = resolve_model_id(str(model_id))
189 except ValueError as exc:
190 return {"error": str(exc)}
191
192 prompts_path = args.get("prompts")
193 try:
194 prompts = load_prompts(prompts_path) if prompts_path else None
195 except (FileNotFoundError, ValueError) as exc:
196 return {"error": str(exc)}
197
198 try:
199 return run_residual_drift(
200 model_id,
201 checkpoint,
202 prompts,
203 checkpoint_name=args.get("name"),
204 )
205 except Exception as exc:
206 return {"error": str(exc)}
list[dict[str, Any]] _layer_drift_rows(dict[int, torch.Tensor] base_acts, dict[int, torch.Tensor] ft_acts, int n_layers)
dict[str, Any] run_residual_drift_from_args(dict[str, Any] args)
dict[str, Any] run_llm_residual_drift(str model_id, str|Path checkpoint_path, list[str] prompts, *, str checkpoint_name="checkpoint")
dict[str, Any] run_residual_drift(str model_id, str|Path checkpoint_path, list[str]|None prompts=None, *, str|None checkpoint_name=None)
torch.Tensor _cosine_distance_rows(torch.Tensor a, torch.Tensor b)
dict[str, Any] _finalize_payload(*, str model_id, str mode, str activation_mode, str checkpoint_name, str checkpoint_path, list[str] prompts, list[dict[str, Any]] layer_profile, list[dict[str, Any]] per_probe, int|None step=None)
list[dict[str, Any]] _per_probe_rows(dict[int, torch.Tensor] base_acts, dict[int, torch.Tensor] ft_acts, list[dict[str, Any]] layer_rows, list[str] probes)