AQIT 0.1.0
Loading...
Searching...
No Matches
attention_routing.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"""Per-head attention routing: sink + induction scores across all layers."""
7
8from __future__ import annotations
9
10from typing import Any
11
12import torch
13
14
15def _count_induction_pairs(token_ids: list[int]) -> int:
16 """Positions (q, j) where token[j] repeats at q and j+1 is valid."""
17 n = 0
18 for q in range(len(token_ids)):
19 for j in range(q):
20 if token_ids[j] == token_ids[q] and j + 1 < len(token_ids):
21 n += 1
22 return n
23
24
25def _score_head(
26 pattern: torch.Tensor,
27 token_ids: list[int],
28) -> tuple[float, float]:
29 """pattern: (seq, seq) causal attn weights for one head."""
30 seq_len = pattern.shape[0]
31
32 if seq_len > 1:
33 sink = float(pattern[1:, 0].mean().item())
34 else:
35 sink = float(pattern[0, 0].item())
36
37 ind_sum = 0.0
38 ind_count = 0
39 for q in range(seq_len):
40 tq = token_ids[q]
41 for j in range(q):
42 if token_ids[j] == tq and j + 1 < seq_len:
43 ind_sum += float(pattern[q, j + 1].item())
44 ind_count += 1
45 induction = ind_sum / max(ind_count, 1)
46 return sink, induction
47
48
50 patterns: list[tuple[int, torch.Tensor]],
51 token_ids: list[int],
52) -> list[dict]:
53 """patterns: list of (layer_idx, tensor [n_heads, seq, seq])."""
54 heads: list[dict] = []
55 for layer, attn in patterns:
56 if attn is None:
57 continue
58 t = attn.detach().float()
59 if t.ndim == 4:
60 t = t[0]
61 if t.ndim != 3:
62 continue
63 n_heads = t.shape[0]
64 for h in range(n_heads):
65 sink, induction = _score_head(t[h], token_ids)
66 heads.append(
67 {
68 "layer": int(layer),
69 "head": int(h),
70 "sink_score": round(sink, 4),
71 "induction_score": round(induction, 4),
72 }
73 )
74 return heads
75
76
77def _patterns_from_tl_cache(model: Any, tokens: torch.Tensor, n_layers: int) -> list[tuple[int, torch.Tensor]]:
78 with torch.no_grad():
79 _, cache = model.run_with_cache(
80 tokens,
81 names_filter=lambda name: name.endswith("attn.hook_pattern"),
82 )
83 out: list[tuple[int, torch.Tensor]] = []
84 for layer in range(n_layers):
85 key = f"blocks.{layer}.attn.hook_pattern"
86 if key not in cache:
87 continue
88 attn = cache[key]
89 if isinstance(attn, torch.Tensor):
90 out.append((layer, attn[0] if attn.ndim == 4 else attn))
91 return out
92
93
95 model: Any, tokens: torch.Tensor
96) -> tuple[list[tuple[int, torch.Tensor]], str | None]:
97 """
98 HF-native / HfLlmShim path.
99 TransformerLens patterns are unavailable on hf_only models (LFM, …).
100 """
101 hf = getattr(model, "hf_model", None)
102 if hf is None:
103 return [], None
104 try:
105 with torch.no_grad():
106 out = hf(
107 input_ids=tokens,
108 output_attentions=True,
109 use_cache=False,
110 return_dict=True,
111 )
112 except TypeError:
113 try:
114 with torch.no_grad():
115 out = hf(input_ids=tokens, output_attentions=True, return_dict=True)
116 except Exception as exc: # noqa: BLE001
117 return [], f"output_attentions failed: {exc}"
118 except Exception as exc: # noqa: BLE001
119 return [], f"output_attentions failed: {exc}"
120
121 attns = getattr(out, "attentions", None)
122 if not attns:
123 return [], (
124 "Model returned no attentions (output_attentions unused or architecture "
125 "does not expose multi-head attention weights)."
126 )
127
128 patterns: list[tuple[int, torch.Tensor]] = []
129 for layer, layer_attn in enumerate(attns):
130 if layer_attn is None:
131 continue # e.g. non-attention / conv hybrid layers
132 patterns.append((layer, layer_attn[0] if layer_attn.ndim == 4 else layer_attn))
133 if not patterns:
134 return [], "All layer attentions were empty (hybrid/conv-only blocks?)."
135 return patterns, None
136
137
139 model: Any,
140 prompt: str,
141 *,
142 top_k: int = 5,
143 model_id: str = "",
144) -> dict:
145 tokens = model.to_tokens(prompt)
146 if tokens.ndim == 1:
147 tokens = tokens.unsqueeze(0)
148 token_ids = tokens[0].tolist()
149 n_layers = int(getattr(getattr(model, "cfg", None), "n_layers", 0) or 0)
150 n_heads = int(getattr(getattr(model, "cfg", None), "n_heads", 0) or 0)
151
152 source = "tl_cache"
153 warning: str | None = None
154 patterns = _patterns_from_tl_cache(model, tokens, n_layers) if n_layers else []
155
156 if not patterns:
157 patterns, warning = _patterns_from_hf_output_attentions(model, tokens)
158 source = "hf_output_attentions"
159
160 heads = _heads_from_pattern_stack(patterns, token_ids)
161
162 if heads and (n_layers <= 0 or n_heads <= 0):
163 n_layers = max(h["layer"] for h in heads) + 1
164 n_heads = max(h["head"] for h in heads) + 1
165
166 by_sink = sorted(heads, key=lambda x: x["sink_score"], reverse=True)
167 by_ind = sorted(heads, key=lambda x: x["induction_score"], reverse=True)
168 n_ind_pairs = _count_induction_pairs(token_ids)
169
170 result: dict[str, Any] = {
171 "prompt": prompt,
172 "model_id": model_id,
173 "n_layers": n_layers,
174 "n_heads": n_heads,
175 "n_induction_pairs": n_ind_pairs,
176 "heads": heads,
177 "top_sink_heads": by_sink[:top_k],
178 "top_induction_heads": by_ind[:top_k] if n_ind_pairs else [],
179 "source": source,
180 }
181 if not heads:
182 result["warning"] = warning or (
183 "No per-head attention patterns available for this model. "
184 "TransformerLens hook_pattern cache was empty and HF output_attentions "
185 "did not return usable weights."
186 )
187 elif warning:
188 result["warning"] = warning
189 return result
list[tuple[int, torch.Tensor]] _patterns_from_tl_cache(Any model, torch.Tensor tokens, int n_layers)
dict run_attention_routing(Any model, str prompt, *, int top_k=5, str model_id="")
list[dict] _heads_from_pattern_stack(list[tuple[int, torch.Tensor]] patterns, list[int] token_ids)
int _count_induction_pairs(list[int] token_ids)
tuple[float, float] _score_head(torch.Tensor pattern, list[int] token_ids)
tuple[list[tuple[int, torch.Tensor]], str|None] _patterns_from_hf_output_attentions(Any model, torch.Tensor tokens)