Coverage for transformer_lens/head_detector.py: 96%

89 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-21 19:27 +0000

1"""Head Detector. 

2 

3Utilities for detecting specific types of heads (e.g. previous token heads). 

4""" 

5 

6import logging 

7from collections import defaultdict 

8from typing import Dict, List, Optional, Tuple, Union, cast 

9 

10import numpy as np 

11import torch 

12from typing_extensions import Literal, get_args 

13 

14from transformer_lens.ActivationCache import ActivationCache 

15from transformer_lens.model_protocol import TransformerLensModel 

16from transformer_lens.utilities import is_lower_triangular, is_square 

17 

18HeadName = Literal["previous_token_head", "duplicate_token_head", "induction_head"] 

19HEAD_NAMES = cast(List[HeadName], get_args(HeadName)) 

20ErrorMeasure = Literal["abs", "mul"] 

21 

22LayerHeadTuple = Tuple[int, int] 

23LayerToHead = Dict[int, List[int]] 

24 

25INVALID_HEAD_NAME_ERR = ( 

26 f"detection_pattern must be a Tensor or one of head names: {HEAD_NAMES}; got %s" 

27) 

28 

29CACHE_WITH_SEQ_LIST_ERR = ( 

30 "A single cache cannot be reused across multiple prompts, so `cache` is not\n" 

31 "supported when `seq` is a list. Pass one prompt at a time, or omit `cache`\n" 

32 "and let each prompt be run separately." 

33) 

34 

35SEQ_LEN_ERR = "The sequence must be non-empty and must fit within the model's context window." 

36 

37DET_PAT_NOT_SQUARE_ERR = "The detection pattern must be a lower triangular matrix of shape (sequence_length, sequence_length); sequence_length=%d; got detection pattern of shape %s" 

38 

39 

40def detect_head( 

41 model: TransformerLensModel, 

42 seq: Union[str, List[str]], 

43 detection_pattern: Union[torch.Tensor, HeadName], 

44 heads: Optional[Union[List[LayerHeadTuple], LayerToHead]] = None, 

45 cache: Optional[ActivationCache] = None, 

46 *, 

47 exclude_bos: bool = False, 

48 exclude_current_token: bool = False, 

49 error_measure: ErrorMeasure = "mul", 

50) -> torch.Tensor: 

51 """Search for a Particular Type of Attention Head. 

52 

53 Searches the model (or a set of specific heads, for circuit analysis) for a particular type of 

54 attention head. This head is specified by a detection pattern, a (sequence_length, 

55 sequence_length) tensor representing the attention pattern we expect that type of attention head 

56 to show. The detection pattern can be also passed not as a tensor, but as a name of one of 

57 pre-specified types of attention head (see `HeadName` for available patterns), in which case the 

58 tensor is computed within the function itself. 

59 

60 There are two error measures available for quantifying the match between the detection pattern 

61 and the actual attention pattern. 

62 

63 1. `"mul"` (default) multiplies both tensors element-wise and divides the sum of the result by 

64 the sum of the attention pattern. Typically, the detection pattern should in this case 

65 contain only ones and zeros, which allows a straightforward interpretation of the score: how 

66 big fraction of this head's attention is allocated to these specific query-key pairs? Using 

67 values other than 0 or 1 is not prohibited but will raise a warning (which can be disabled, 

68 of course). 

69 

70 2. `"abs"` calculates the mean element-wise absolute difference between the detection pattern 

71 and the actual attention pattern. The "raw result" ranges from 0 to 2 where lower score 

72 corresponds to greater accuracy. Subtracting it from 1 maps that range to (-1, 1) interval, 

73 with 1 being perfect match and -1 perfect mismatch. 

74 

75 Which one should you use? 

76 

77 `"mul"` is likely better for quick or exploratory investigations. For precise examinations where 

78 you're trying to reproduce as much functionality as possible or really test your understanding 

79 of the attention head, you probably want to switch to `"abs"`. 

80 

81 The advantage of `"abs"` is that you can make more precise predictions, and have that measured 

82 in the score. You can predict, for instance, 0.2 attention to X, and 0.8 attention to Y, and 

83 your score will be better if your prediction is closer. The "mul" metric does not allow this, 

84 you'll get the same score if attention is 0.2, 0.8 or 0.5, 0.5 or 0.8, 0.2. 

85 

86 Args: 

87 model: Model being used. 

88 seq: String or list of strings being fed to the model. 

89 head_name: Name of an existing head in HEAD_NAMES we want to check. Must pass either a 

90 head_name or a detection_pattern, but not both! 

91 detection_pattern: (sequence_length, sequence_length)nTensor representing what attention 

92 pattern corresponds to the head we're looking for or the name of a pre-specified head. 

93 Currently available heads are: `["previous_token_head", "duplicate_token_head", 

94 "induction_head"]`. 

95 heads: If specific attention heads is given here, all other heads' score is set to -1. 

96 Useful for IOI-style circuit analysis. Heads can be specified as a list of tuples (layer, 

97 head) or a dictionary mapping a layer to heads within that layer that we want to 

98 analyze. cache: Include the cache to save time if you want. 

99 exclude_bos: Exclude attention paid to the beginning of sequence token. 

100 exclude_current_token: Exclude attention paid to the current token. 

101 error_measure: `"mul"` for using element-wise multiplication. `"abs"` for using absolute 

102 values of element-wise differences as the error measure. 

103 

104 Returns: 

105 Tensor representing the score for each attention head. 

106 """ 

107 

108 cfg = model.cfg 

109 tokens = model.to_tokens(seq).to(cfg.device) 

110 seq_len = tokens.shape[-1] 

111 

112 # Validate error_measure 

113 

114 assert error_measure in get_args( 

115 ErrorMeasure 

116 ), f"Invalid error_measure={error_measure}; valid values are {get_args(ErrorMeasure)}" 

117 

118 # Validate detection pattern if it's a string 

119 if isinstance(detection_pattern, str): 

120 assert detection_pattern in HEAD_NAMES, INVALID_HEAD_NAME_ERR % detection_pattern 

121 if isinstance(seq, list): 

122 # Every other argument is forwarded below. `cache` deliberately is 

123 # not, because one cache holds the activations of one prompt and 

124 # cannot serve the rest. Say so rather than dropping it quietly. 

125 if cache is not None: 

126 raise ValueError(CACHE_WITH_SEQ_LIST_ERR) 

127 batch_scores = [ 

128 detect_head( 

129 model, 

130 batch_seq, 

131 detection_pattern, 

132 heads=heads, 

133 exclude_bos=exclude_bos, 

134 exclude_current_token=exclude_current_token, 

135 error_measure=error_measure, 

136 ) 

137 for batch_seq in seq 

138 ] 

139 return torch.stack(batch_scores).mean(0) 

140 detection_pattern = cast( 

141 torch.Tensor, 

142 eval(f"get_{detection_pattern}_detection_pattern(tokens.cpu())"), 

143 ).to(cfg.device) 

144 

145 # if we're using "mul", detection_pattern should consist of zeros and ones 

146 if error_measure == "mul" and not set(detection_pattern.unique().tolist()).issubset({0, 1}): 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true

147 logging.warning( 

148 "Using detection pattern with values other than 0 or 1 with error_measure 'mul'" 

149 ) 

150 

151 # Validate inputs and detection pattern shape 

152 assert 1 < tokens.shape[-1] < cfg.n_ctx, SEQ_LEN_ERR 

153 assert ( 

154 is_lower_triangular(detection_pattern) and seq_len == detection_pattern.shape[0] 

155 ), DET_PAT_NOT_SQUARE_ERR % (seq_len, detection_pattern.shape) 

156 

157 if cache is None: 

158 _, cache = model.run_with_cache(tokens, remove_batch_dim=True) 

159 

160 if heads is None: 

161 layer2heads = {layer_i: list(range(cfg.n_heads)) for layer_i in range(cfg.n_layers)} 

162 elif isinstance(heads, list): 162 ↛ 167line 162 didn't jump to line 167 because the condition on line 162 was always true

163 layer2heads = defaultdict(list) 

164 for layer, head in heads: 

165 layer2heads[layer].append(head) 

166 else: 

167 layer2heads = heads 

168 

169 matches = -torch.ones(cfg.n_layers, cfg.n_heads, dtype=cfg.dtype) 

170 

171 for layer, layer_heads in layer2heads.items(): 

172 # [n_heads q_pos k_pos] 

173 layer_attention_patterns = cache["pattern", layer, "attn"] 

174 for head in layer_heads: 

175 head_attention_pattern = layer_attention_patterns[head, :, :] 

176 head_score = compute_head_attention_similarity_score( 

177 head_attention_pattern, 

178 detection_pattern=detection_pattern, 

179 exclude_bos=exclude_bos, 

180 exclude_current_token=exclude_current_token, 

181 error_measure=error_measure, 

182 ) 

183 matches[layer, head] = head_score 

184 return matches 

185 

186 

187# Previous token head 

188def get_previous_token_head_detection_pattern( 

189 tokens: torch.Tensor, # [batch (1) x pos] 

190) -> torch.Tensor: 

191 """Outputs a detection score for [previous token heads](https://dynalist.io/d/n2ZWtnoYHrU1s4vnFSAQ519J#z=0O5VOHe9xeZn8Ertywkh7ioc). 

192 

193 Args: 

194 tokens: Tokens being fed to the model. 

195 """ 

196 detection_pattern = torch.zeros(tokens.shape[-1], tokens.shape[-1]) 

197 # Adds a diagonal of 1's below the main diagonal. 

198 detection_pattern[1:, :-1] = torch.eye(tokens.shape[-1] - 1) 

199 return torch.tril(detection_pattern) 

200 

201 

202# Duplicate token head 

203def get_duplicate_token_head_detection_pattern( 

204 tokens: torch.Tensor, # [batch (1) x pos] 

205) -> torch.Tensor: 

206 """Outputs a detection score for [duplicate token heads](https://dynalist.io/d/n2ZWtnoYHrU1s4vnFSAQ519J#z=2UkvedzOnghL5UHUgVhROxeo). 

207 

208 Args: 

209 sequence: String being fed to the model. 

210 """ 

211 # [pos x pos] 

212 token_pattern = tokens.repeat(tokens.shape[-1], 1).numpy() 

213 

214 # If token_pattern[i][j] matches its transpose, then token j and token i are duplicates. 

215 eq_mask = np.equal(token_pattern, token_pattern.T).astype(int) 

216 

217 np.fill_diagonal(eq_mask, 0) # Current token is always a duplicate of itself. Ignore that. 

218 detection_pattern = eq_mask.astype(int) 

219 return torch.tril(torch.as_tensor(detection_pattern).float()) 

220 

221 

222# Induction head 

223def get_induction_head_detection_pattern( 

224 tokens: torch.Tensor, # [batch (1) x pos] 

225) -> torch.Tensor: 

226 """Outputs a detection score for [induction heads](https://dynalist.io/d/n2ZWtnoYHrU1s4vnFSAQ519J#z=_tFVuP5csv5ORIthmqwj0gSY). 

227 

228 Args: 

229 sequence: String being fed to the model. 

230 """ 

231 duplicate_pattern = get_duplicate_token_head_detection_pattern(tokens) 

232 

233 # Shift all items one to the right 

234 shifted_tensor = torch.roll(duplicate_pattern, shifts=1, dims=1) 

235 

236 # Replace first column with 0's 

237 # we don't care about bos but shifting to the right moves the last column to the first, 

238 # and the last column might contain non-zero values. 

239 zeros_column = torch.zeros(duplicate_pattern.shape[0], 1) 

240 result_tensor = torch.cat((zeros_column, shifted_tensor[:, 1:]), dim=1) 

241 return torch.tril(result_tensor) 

242 

243 

244def get_supported_heads() -> None: 

245 """Print the supported head names.""" 

246 print(f"Supported heads: {HEAD_NAMES}") 

247 

248 

249def compute_head_attention_similarity_score( 

250 attention_pattern: torch.Tensor, # [q_pos k_pos] 

251 detection_pattern: torch.Tensor, # [seq_len seq_len] (seq_len == q_pos == k_pos) 

252 *, 

253 exclude_bos: bool, 

254 exclude_current_token: bool, 

255 error_measure: ErrorMeasure, 

256) -> float: 

257 """Compute the similarity between `attention_pattern` and `detection_pattern`. 

258 

259 Args: 

260 attention_pattern: Lower triangular matrix (Tensor) representing the attention pattern of a particular attention head. 

261 detection_pattern: Lower triangular matrix (Tensor) representing the attention pattern we are looking for. 

262 exclude_bos: `True` if the beginning-of-sentence (BOS) token should be omitted from comparison. `False` otherwise. 

263 exclude_bcurrent_token: `True` if the current token at each position should be omitted from comparison. `False` otherwise. 

264 error_measure: "abs" for using absolute values of element-wise differences as the error measure. "mul" for using element-wise multiplication (legacy code). 

265 """ 

266 assert is_square( 

267 attention_pattern 

268 ), f"Attention pattern is not square; got shape {attention_pattern.shape}" 

269 

270 # mul 

271 

272 if error_measure == "mul": 

273 # Clone before masking. attention_pattern is a view into the caller's 

274 # ActivationCache, so masking in place permanently corrupts the cached 

275 # pattern: its rows stop summing to 1 and every later use of that cache 

276 # silently reads modified activations. 

277 if exclude_bos or exclude_current_token: 

278 attention_pattern = attention_pattern.clone() 

279 if exclude_bos: 

280 attention_pattern[:, 0] = 0 

281 if exclude_current_token: 

282 attention_pattern.fill_diagonal_(0) 

283 score = attention_pattern * detection_pattern 

284 return (score.sum() / attention_pattern.sum()).item() 

285 

286 # abs 

287 

288 abs_diff = (attention_pattern - detection_pattern).abs() 

289 assert (abs_diff - torch.tril(abs_diff).to(abs_diff.device)).sum() == 0 

290 

291 size = len(abs_diff) 

292 if exclude_bos: 

293 abs_diff[:, 0] = 0 

294 if exclude_current_token: 

295 abs_diff.fill_diagonal_(0) 

296 

297 return 1 - round((abs_diff.mean() * size).item(), 3) 

298 return 1 - round((abs_diff.mean() * size).item(), 3)