AQIT 0.1.0
Loading...
Searching...
No Matches
causal_trace.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2# This file is part of the Aquin Engine. Unauthorized copying, modification,
3# distribution, or use of this file, via any medium, is strictly prohibited.
4# Proprietary and confidential. See LICENSE for terms.
5
6"""
7Ingested from inspection-backend/causal_trace.py.
8Import adaptation only: model_config -> aquin.compute.model_loader.
9load_model() delegates to aquin.compute.model_loader (same instance as aquin load).
10"""
11from __future__ import annotations
12
13from collections import OrderedDict
14from typing import Any
15
16import torch
17from transformer_lens import HookedTransformer
18
19from aquin.compute.model_loader import get_config, load_model, resolve_model_id
20
21from aquin.compute.device import (
22 default_dtype_for_device,
23 resolve_compute_device,
24 synchronize_device,
25)
26
27DEVICE = resolve_compute_device()
28DTYPE = default_dtype_for_device(DEVICE)
29
30
31def _project_unembed(model, resid: torch.Tensor) -> torch.Tensor:
32 """Map residual vector to vocab logits (handles tied vs untied lm_head layouts)."""
33 from aquin.compute.hf_llm_shim import project_residual_to_logits
34
35 return project_residual_to_logits(model, resid)
36
37
38def _format_prompt(model: HookedTransformer, prompt: str) -> str:
39 if hasattr(model.tokenizer, "chat_template") and model.tokenizer.chat_template:
40 messages = [{"role": "user", "content": prompt}]
41 return model.tokenizer.apply_chat_template(
42 messages, tokenize=False, add_generation_prompt=True
43 )
44 return prompt
45
46
47def _get_embeds(tokens, model_id: str = "llama-3.2-1b"):
48 m = load_model(model_id)
49 with torch.no_grad():
50 return m.W_E[tokens]
52
53def _corrupt_all(tokens, noise_scale, model_id: str = "llama-3.2-1b"):
54 embeds = _get_embeds(tokens, model_id)
55 return embeds + torch.randn_like(embeds) * noise_scale
56
58def _corrupt_single_pos(tokens, pos, noise_scale, model_id: str = "llama-3.2-1b"):
59 embeds = _get_embeds(tokens, model_id)
60 noise = torch.zeros_like(embeds)
61 noise[0, pos] = torch.randn(embeds.shape[-1], device=embeds.device) * noise_scale
62 return embeds + noise
63
64
65def run_trace(
66 prompt: str,
67 target_token: str,
68 model_id: str = "llama-3.2-1b",
69 noise_scale: float = 3.0,
70 n_noise_runs: int = 10,
71):
72 m = load_model(model_id)
73 tokens = m.to_tokens(prompt)
74 target_id = m.to_tokens(target_token, prepend_bos=False)[0, 0].item()
75
76 with torch.no_grad():
77 clean_logits, clean_cache = m.run_with_cache(tokens)
78 baseline_prob = torch.softmax(clean_logits[0, -1], dim=-1)[target_id].item()
79
80 corrupted_prob = 0.0
81 for _ in range(n_noise_runs):
82 ne = _corrupt_all(tokens, noise_scale, model_id)
83 with torch.no_grad():
84 cl = m.run_with_hooks(tokens, fwd_hooks=[("hook_embed", lambda v, hook=None: ne.to(v.device))])
85 corrupted_prob += torch.softmax(cl[0, -1], dim=-1)[target_id].item()
86 corrupted_prob /= n_noise_runs
87 corruption_effect = baseline_prob - corrupted_prob
88
89 results = []
90 for layer in range(m.cfg.n_layers):
91 patch_probs = []
92 for _ in range(n_noise_runs):
93 ne = _corrupt_all(tokens, noise_scale, model_id)
94 def make_hooks(ne_, li):
95 return [
96 ("hook_embed", lambda v, hook=None, n=ne_: n.to(v.device)),
97 (f"blocks.{li}.hook_resid_post", lambda v, hook=None, li=li: (
98 v.__setitem__(
99 (slice(None), -1, slice(None)),
100 clean_cache[f"blocks.{li}.hook_resid_post"][:, -1, :].to(v.device)
101 ) or v
102 )),
103 ]
104 with torch.no_grad():
105 pl = m.run_with_hooks(tokens, fwd_hooks=make_hooks(ne, layer))
106 patch_probs.append(torch.softmax(pl[0, -1], dim=-1)[target_id].item())
107
108 restored = sum(patch_probs) / len(patch_probs)
109 drop = round(max(restored - corrupted_prob, 0.0) / max(corruption_effect, 1e-6), 4)
110 results.append({
111 "layer": layer,
112 "baseline_prob": round(baseline_prob, 4),
113 "corrupted_prob": round(corrupted_prob, 4),
114 "corruption_effect": round(corruption_effect, 4),
115 "drop": drop,
116 "attn_drop": 0.0,
117 "mlp_drop": 0.0,
118 "patched_prob": round(restored, 4),
119 "attn_patched_prob": 0.0,
120 "mlp_patched_prob": 0.0,
121 })
122
123 synchronize_device()
124 return sorted(results, key=lambda x: x["drop"], reverse=True)
125
126
128 prompt, response, prompt_tokens, response_tokens,
129 sig_prompt_tis, sig_response_tis,
130 model_id: str = "llama-3.2-1b",
131 noise_scale: float = 3.0,
132 n_noise_runs: int = 5,
133):
134 m = load_model(model_id)
135 full_ctx = f"{prompt}\n{response}"
136 tokens = m.to_tokens(full_ctx)
137 n_tokens = tokens.shape[1]
138
139 with torch.no_grad():
140 clean_logits = m(tokens)
141
142 target_ids, baseline_probs = {}, {}
143 for rti in sig_response_tis:
144 tok_str = f" {response_tokens[rti].strip()}"
145 try:
146 tid = m.to_tokens(tok_str, prepend_bos=False)[0, 0].item()
147 except Exception:
148 continue
149 target_ids[rti] = tid
150 baseline_probs[rti] = torch.softmax(clean_logits[0, -1], dim=-1)[tid].item()
151
152 if not target_ids:
153 return {"attributions": []}
154
155 prefix_len = m.to_tokens(prompt + "\n", prepend_bos=True).shape[1]
156
157 def get_token_pos(word_idx):
158 words = prompt.split()
159 if word_idx >= len(words):
160 return None
161 char_pos = sum(len(w) + 1 for w in words[:word_idx])
162 try:
163 return m.to_tokens(prompt[:char_pos], prepend_bos=True).shape[1] - 1
164 except Exception:
165 return None
166
167 scores = {rti: {} for rti in target_ids}
168 for pti in sig_prompt_tis:
169 tok_pos = get_token_pos(pti)
170 if tok_pos is None or tok_pos >= n_tokens:
171 continue
172
173 corrupted_probs = {rti: 0.0 for rti in target_ids}
174 for _ in range(n_noise_runs):
175 ne = _corrupt_single_pos(tokens, tok_pos, noise_scale, model_id)
176 with torch.no_grad():
177 logits = m.run_with_hooks(
178 tokens,
179 fwd_hooks=[("hook_embed", lambda v, hook=None, n=ne: n.to(v.device))]
180 )
181 probs = torch.softmax(logits[0, -1], dim=-1)
182 for rti, tid in target_ids.items():
183 corrupted_probs[rti] += probs[tid].item()
184
185 for rti in target_ids:
186 corrupted_probs[rti] /= n_noise_runs
187 scores[rti][pti] = max(round(baseline_probs[rti] - corrupted_probs[rti], 4), 0.0)
188
189 attributions = []
190 for rti, tid in target_ids.items():
191 raw = scores[rti]
192 mx = max(raw.values(), default=1e-6) or 1e-6
193 prompt_scores = sorted([
194 {
195 "prompt_ti": pti,
196 "prompt_token": prompt_tokens[pti],
197 "score": round(raw.get(pti, 0.0) / mx, 4),
198 "raw_score": raw.get(pti, 0.0),
199 }
200 for pti in sig_prompt_tis
201 ], key=lambda x: x["score"], reverse=True)
202 attributions.append({
203 "response_ti": rti,
204 "response_token": response_tokens[rti],
205 "baseline_prob": round(baseline_probs[rti], 4),
206 "prompt_scores": prompt_scores,
207 })
208
209 synchronize_device()
210 return {"attributions": attributions}
211
212
214 prompt: str,
215 model_id: str = "llama-3.2-1b",
216 top_k: int = 5,
218 m = load_model(model_id)
219 tokens = m.to_tokens(prompt)
220 with torch.no_grad():
221 _, cache = m.run_with_cache(tokens)
222
223 results = []
224 for layer in range(m.cfg.n_layers):
225 resid = cache[f"blocks.{layer}.hook_resid_post"][0, -1, :]
226 resid_normed = m.ln_final(resid.unsqueeze(0).unsqueeze(0))[0, 0, :]
227 logits = _project_unembed(m, resid_normed)
228 probs = torch.softmax(logits, dim=-1)
229 topk = probs.topk(top_k)
230 results.append({
231 "layer": layer,
232 "top_tokens": [
233 {"token": m.to_string([tid.item()]), "prob": round(p.item(), 4)}
234 for tid, p in zip(topk.indices, topk.values)
235 ],
236 })
237
238 synchronize_device()
239 return results
240
241
242def _resolve_chat_model(model_id: str, model: Any | None = None):
243 from aquin.compute.model_loader import get_loaded_model, load_model, resolve_model_id
244
245 mid = resolve_model_id(model_id)
246 m = model if model is not None else get_loaded_model()
247 if m is None:
248 m = load_model(mid)
249 return m
250
251
252def _generation_device(model: Any) -> torch.device:
253 if hasattr(model, "hf_model"):
254 embed = model.hf_model.get_input_embeddings()
255 if embed is not None and hasattr(embed, "weight"):
256 return embed.weight.device
257 dev = getattr(model, "device", None)
258 if dev is not None:
259 return dev if isinstance(dev, torch.device) else torch.device(str(dev))
260 return torch.device(DEVICE)
261
262
263def _decode_chat_completion(model: Any, input_ids: torch.Tensor, output_ids: torch.Tensor) -> str:
264 prompt_len = int(input_ids.shape[-1])
265 new_tokens = output_ids[0, prompt_len:]
266 return model.tokenizer.decode(new_tokens.tolist(), skip_special_tokens=True).strip()
268
269def run_chat(
270 prompt: str,
271 model_id: str = "llama-3.2-1b",
272 max_new_tokens: int = 200,
273 temperature: float = 0.7,
274 model: Any | None = None,
275) -> str:
276 m = _resolve_chat_model(model_id, model)
277 formatted = _format_prompt(m, prompt)
278 device = _generation_device(m)
279 input_ids = m.tokenizer(formatted, return_tensors="pt").input_ids.to(device)
280
281 if hasattr(m, "hf_model"):
282 tok = m.tokenizer
283 if tok.pad_token_id is None and tok.eos_token_id is not None:
284 tok.pad_token_id = tok.eos_token_id
285 pad_id = tok.pad_token_id if tok.pad_token_id is not None else tok.eos_token_id
286 gen_kwargs: dict[str, Any] = {
287 "max_new_tokens": max_new_tokens,
288 "do_sample": temperature > 0,
289 }
290 if pad_id is not None:
291 gen_kwargs["pad_token_id"] = pad_id
292 if temperature > 0:
293 gen_kwargs["temperature"] = temperature
294 with torch.no_grad():
295 output_ids = m.hf_model.generate(input_ids, **gen_kwargs)
296 return _decode_chat_completion(m, input_ids, output_ids)
297
298 eos_id = getattr(m.tokenizer, "eos_token_id", None)
299 generated: list[int] = []
300
301 with torch.no_grad():
302 cur = input_ids
303 for _ in range(max_new_tokens):
304 logits = m(cur)
305 row = logits[0, -1]
306 if temperature <= 0:
307 next_id = int(row.argmax().item())
308 else:
309 probs = torch.softmax(row / temperature, dim=-1)
310 next_id = int(torch.multinomial(probs, 1).item())
311 if eos_id is not None and next_id == eos_id:
312 break
313 generated.append(next_id)
314 cur = torch.cat(
315 [cur, torch.tensor([[next_id]], device=device, dtype=cur.dtype)],
316 dim=1,
317 )
318
319 return m.tokenizer.decode(generated, skip_special_tokens=True).strip()
320
321
322def stream_chat(
323 prompt: str,
324 model_id: str = "llama-3.2-1b",
325 max_new_tokens: int = 200,
326 temperature: float = 0.7,
327):
328 m = load_model(model_id)
329 formatted = _format_prompt(m, prompt)
330 input_ids = m.tokenizer(formatted, return_tensors="pt").input_ids.to(DEVICE)
331
332 with torch.no_grad():
333 cur = input_ids
334 for _ in range(max_new_tokens):
335 logits = m(cur)
336 next_logits = logits[0, -1] / max(temperature, 1e-6)
337 probs = torch.softmax(next_logits, dim=-1)
338 next_id = int(torch.multinomial(probs, 1).item())
339 if next_id == m.tokenizer.eos_token_id:
340 break
341 tok_str = m.tokenizer.decode([next_id], skip_special_tokens=True)
342 yield tok_str
343 cur = torch.cat([cur, torch.tensor([[next_id]], device=DEVICE)], dim=1)
_get_embeds(tokens, str model_id="llama-3.2-1b")
run_prompt_attribution(prompt, response, prompt_tokens, response_tokens, sig_prompt_tis, sig_response_tis, str model_id="llama-3.2-1b", float noise_scale=3.0, int n_noise_runs=5)
run_logit_lens(str prompt, str model_id="llama-3.2-1b", int top_k=5)
_corrupt_single_pos(tokens, pos, noise_scale, str model_id="llama-3.2-1b")
run_trace(str prompt, str target_token, str model_id="llama-3.2-1b", float noise_scale=3.0, int n_noise_runs=10)
torch.device _generation_device(Any model)
str run_chat(str prompt, str model_id="llama-3.2-1b", int max_new_tokens=200, float temperature=0.7, Any|None model=None)
str _decode_chat_completion(Any model, torch.Tensor input_ids, torch.Tensor output_ids)
stream_chat(str prompt, str model_id="llama-3.2-1b", int max_new_tokens=200, float temperature=0.7)
str _format_prompt(HookedTransformer model, str prompt)
_corrupt_all(tokens, noise_scale, str model_id="llama-3.2-1b")
_resolve_chat_model(str model_id, Any|None model=None)
torch.Tensor _project_unembed(model, torch.Tensor resid)