Coverage for transformer_lens/lit/utils.py: 55%

122 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-01 16:23 +0000

1"""Utility functions for the LIT integration module. 

2 

3This module provides helper functions for converting between TransformerLens 

4data structures and LIT-compatible formats, as well as other utilities. 

5 

6References: 

7 - LIT API: https://pair-code.github.io/lit/documentation/api 

8 - TransformerLens: https://github.com/TransformerLensOrg/TransformerLens 

9""" 

10 

11from __future__ import annotations 

12 

13import logging 

14from typing import Any, Dict, List, Optional, Tuple, Union 

15 

16import numpy as np 

17import torch 

18 

19logger = logging.getLogger(__name__) 

20 

21 

22def check_lit_installed() -> bool: 

23 """Check if LIT (lit-nlp) is installed. 

24 

25 Returns: 

26 bool: True if LIT is installed, False otherwise. 

27 """ 

28 try: 

29 import lit_nlp # noqa: F401 

30 

31 return True 

32 except ImportError: 

33 return False 

34 

35 

36def tensor_to_numpy( 

37 tensor: Union[torch.Tensor, np.ndarray, None], 

38) -> Optional[np.ndarray]: 

39 """Convert a PyTorch tensor to a NumPy array. 

40 

41 LIT expects all data to be in NumPy format, so this helper ensures 

42 proper conversion with detach and CPU transfer. 

43 

44 Args: 

45 tensor: PyTorch tensor or None. 

46 

47 Returns: 

48 NumPy array or None if input was None. 

49 """ 

50 if tensor is None: 

51 return None 

52 if isinstance(tensor, np.ndarray): 

53 return tensor 

54 if isinstance(tensor, torch.Tensor): 54 ↛ 56line 54 didn't jump to line 56 because the condition on line 54 was always true

55 return tensor.detach().cpu().numpy() 

56 raise TypeError(f"Expected torch.Tensor or np.ndarray, got {type(tensor)}") 

57 

58 

59def numpy_to_tensor( 

60 array: Union[np.ndarray, torch.Tensor, None], 

61 device: Optional[Union[str, torch.device]] = None, 

62 dtype: Optional[torch.dtype] = None, 

63) -> Optional[torch.Tensor]: 

64 """Convert a NumPy array to a PyTorch tensor. 

65 

66 Args: 

67 array: NumPy array or None. 

68 device: Target device for the tensor. 

69 dtype: Target dtype for the tensor. 

70 

71 Returns: 

72 PyTorch tensor or None if input was None. 

73 """ 

74 if array is None: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true

75 return None 

76 if isinstance(array, torch.Tensor): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true

77 tensor = array 

78 else: 

79 tensor = torch.from_numpy(array) 

80 

81 if dtype is not None: 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true

82 tensor = tensor.to(dtype) 

83 if device is not None: 

84 tensor = tensor.to(device) 

85 return tensor 

86 

87 

88def get_tokens_from_model( 

89 model: Any, 

90 text: str, 

91 prepend_bos: bool = True, 

92 truncate: bool = True, 

93 max_length: Optional[int] = None, 

94) -> Tuple[List[str], torch.Tensor]: 

95 """Get tokens and token IDs from a HookedTransformer model. 

96 

97 Args: 

98 model: HookedTransformer model with tokenizer. 

99 text: Input text to tokenize. 

100 prepend_bos: Whether to prepend the BOS token. 

101 truncate: Whether to truncate to max_length. 

102 max_length: Maximum sequence length. 

103 

104 Returns: 

105 Tuple of (token strings, token ID tensor). 

106 

107 Raises: 

108 ValueError: If model has no tokenizer. 

109 """ 

110 if model.tokenizer is None: 

111 raise ValueError("Model must have a tokenizer to convert text to tokens") 

112 

113 token_ids = model.to_tokens(text, prepend_bos=prepend_bos, truncate=truncate) 

114 

115 if max_length is not None and token_ids.shape[1] > max_length: 

116 token_ids = token_ids[:, :max_length] 

117 

118 token_strings = model.tokenizer.convert_ids_to_tokens(token_ids.squeeze(0).tolist()) 

119 

120 return token_strings, token_ids.squeeze(0) 

121 

122 

123def clean_token_string(token: str) -> str: 

124 """Clean a token string for display. 

125 

126 Handles common tokenizer artifacts like: 

127 - Ġ (GPT-2 style space prefix) 

128 - ▁ (SentencePiece space prefix) 

129 - ## (BERT style subword prefix) 

130 

131 Args: 

132 token: Raw token string from tokenizer. 

133 

134 Returns: 

135 Cleaned token string for display. 

136 """ 

137 # Handle GPT-2/RoBERTa style space encoding 

138 if token.startswith("Ġ"): 

139 return "▁" + token[1:] # Use Unicode space indicator 

140 # Handle SentencePiece 

141 if token.startswith("▁"): 

142 return token # Already in preferred format 

143 # Handle BERT style 

144 if token.startswith("##"): 

145 return token[2:] # Remove ## prefix 

146 return token 

147 

148 

149def clean_token_strings(tokens: List[str]) -> List[str]: 

150 """Clean a list of token strings for display. 

151 

152 Args: 

153 tokens: List of raw token strings. 

154 

155 Returns: 

156 List of cleaned token strings. 

157 """ 

158 return [clean_token_string(t) for t in tokens] 

159 

160 

161def extract_attention_from_cache( 

162 cache: Any, 

163 layer: int, 

164 head: Optional[int] = None, 

165 batch_idx: int = 0, 

166) -> Optional[np.ndarray]: 

167 """Extract attention patterns from an activation cache. 

168 

169 Args: 

170 cache: TransformerLens ActivationCache object. 

171 layer: Layer index to extract from. 

172 head: Optional head index. If None, returns all heads. 

173 batch_idx: Batch index to extract. 

174 

175 Returns: 

176 Attention pattern as numpy array. 

177 Shape: [query_pos, key_pos] if head specified 

178 Shape: [num_heads, query_pos, key_pos] if head is None 

179 """ 

180 attn_pattern = cache[f"blocks.{layer}.attn.hook_pattern"] 

181 

182 # Remove batch dimension 

183 if attn_pattern.dim() == 4: 

184 attn_pattern = attn_pattern[batch_idx] 

185 

186 # attn_pattern shape: [num_heads, query_pos, key_pos] 

187 if head is not None: 

188 attn_pattern = attn_pattern[head] 

189 

190 return tensor_to_numpy(attn_pattern) 

191 

192 

193def extract_embeddings_from_cache( 

194 cache: Any, 

195 layer: int, 

196 position: str = "all", 

197 batch_idx: int = 0, 

198) -> Optional[np.ndarray]: 

199 """Extract embeddings from a specific layer in the activation cache. 

200 

201 Args: 

202 cache: TransformerLens ActivationCache object. 

203 layer: Layer index to extract from. 

204 position: "all" for all positions, "first" for CLS-like, "last" for final token. 

205 batch_idx: Batch index to extract. 

206 

207 Returns: 

208 Embeddings as numpy array. 

209 """ 

210 resid = cache[f"blocks.{layer}.hook_resid_post"] 

211 

212 # Remove batch dimension 

213 if resid.dim() == 3: 

214 resid = resid[batch_idx] 

215 

216 # resid shape: [seq_len, d_model] 

217 if position == "first": 

218 embeddings = resid[0] 

219 elif position == "last": 

220 embeddings = resid[-1] 

221 elif position == "mean": 

222 embeddings = resid.mean(dim=0) 

223 else: # "all" 

224 embeddings = resid 

225 

226 return tensor_to_numpy(embeddings) 

227 

228 

229def compute_token_gradients( 

230 model: Any, 

231 text: str, 

232 target_idx: Optional[int] = None, 

233 prepend_bos: bool = True, 

234) -> Tuple[Optional[np.ndarray], Optional[np.ndarray], List[str]]: 

235 """Compute token-level gradients for salience. 

236 

237 Uses gradient of the loss with respect to token embeddings to compute 

238 importance scores for each token. 

239 

240 Args: 

241 model: HookedTransformer model. 

242 text: Input text. 

243 target_idx: Target token index for gradient computation. 

244 If None, uses the last token. 

245 prepend_bos: Whether to prepend BOS token. 

246 

247 Returns: 

248 Tuple of (grad_l2, grad_dot_input, tokens) where: 

249 - grad_l2: L2 norm of gradients per token [seq_len] 

250 - grad_dot_input: Gradient dot input embedding per token [seq_len] 

251 - tokens: List of token strings 

252 """ 

253 # Tokenize 

254 tokens, token_ids = get_tokens_from_model(model, text, prepend_bos=prepend_bos) 

255 token_ids = token_ids.unsqueeze(0).to(model.cfg.device) 

256 

257 input_embeds = model.embed(token_ids) 

258 input_embeds.requires_grad_(True) 

259 

260 # Forward pass 

261 logits = model(input_embeds, start_at_layer=0) 

262 

263 # Determine target 

264 if target_idx is None: 

265 target_idx = -1 # Last token 

266 

267 # Get target logit and compute gradient 

268 target_logit = logits[0, target_idx, token_ids[0, target_idx + 1]] 

269 target_logit.backward() 

270 

271 gradients = input_embeds.grad[0] # [seq_len, d_model] 

272 

273 # Compute gradient L2 norm per token 

274 grad_l2 = torch.norm(gradients, dim=-1) # [seq_len] 

275 

276 # Compute gradient dot input 

277 grad_dot_input = (gradients * input_embeds[0].detach()).sum(dim=-1) # [seq_len] 

278 

279 return ( 

280 tensor_to_numpy(grad_l2), 

281 tensor_to_numpy(grad_dot_input), 

282 tokens, 

283 ) 

284 

285 

286def get_top_k_predictions( 

287 logits: torch.Tensor, 

288 tokenizer: Any, 

289 k: int = 10, 

290 position: int = -1, 

291 batch_idx: int = 0, 

292) -> List[Tuple[str, float]]: 

293 """Get top-k token predictions with their probabilities. 

294 

295 Args: 

296 logits: Model logits tensor. 

297 tokenizer: HuggingFace tokenizer. 

298 k: Number of top predictions to return. 

299 position: Position index to get predictions for. 

300 batch_idx: Batch index. 

301 

302 Returns: 

303 List of (token_string, probability) tuples. 

304 """ 

305 pos_logits = logits[batch_idx, position] # [d_vocab] 

306 

307 probs = torch.softmax(pos_logits, dim=-1) 

308 

309 top_probs, top_indices = torch.topk(probs, k) 

310 

311 # Convert to strings 

312 results = [] 

313 for prob, idx in zip(top_probs.tolist(), top_indices.tolist()): 

314 token_str = tokenizer.decode([idx]) 

315 results.append((token_str, prob)) 

316 

317 return results 

318 

319 

320def validate_input_example( 

321 example: Dict[str, Any], 

322 required_fields: List[str], 

323) -> bool: 

324 """Validate that an input example has all required fields. 

325 

326 Args: 

327 example: Input example dictionary. 

328 required_fields: List of required field names. 

329 

330 Returns: 

331 True if valid, False otherwise. 

332 """ 

333 for field in required_fields: 

334 if field not in example: 

335 logger.warning(f"Missing required field '{field}' in input example") 

336 return False 

337 return True 

338 

339 

340def batch_examples( 

341 examples: List[Dict[str, Any]], 

342 batch_size: int, 

343) -> List[List[Dict[str, Any]]]: 

344 """Split examples into batches. 

345 

346 Args: 

347 examples: List of example dictionaries. 

348 batch_size: Size of each batch. 

349 

350 Returns: 

351 List of batches, where each batch is a list of examples. 

352 """ 

353 return [examples[i : i + batch_size] for i in range(0, len(examples), batch_size)] 

354 

355 

356def unbatch_outputs( 

357 batched_outputs: Dict[str, np.ndarray], 

358) -> List[Dict[str, Any]]: 

359 """Split batched outputs into individual examples. 

360 

361 Takes a dictionary with batched arrays and returns a list of 

362 dictionaries with individual arrays. 

363 

364 Args: 

365 batched_outputs: Dictionary mapping field names to batched arrays. 

366 

367 Returns: 

368 List of dictionaries, one per example. 

369 """ 

370 if not batched_outputs: 370 ↛ 371line 370 didn't jump to line 371 because the condition on line 370 was never true

371 return [] 

372 

373 # Get batch size from first array 

374 first_key = next(iter(batched_outputs)) 

375 batch_size = len(batched_outputs[first_key]) 

376 

377 # Split into individual examples 

378 results = [] 

379 for i in range(batch_size): 

380 example_output = {} 

381 for key, value in batched_outputs.items(): 

382 if isinstance(value, (np.ndarray, torch.Tensor)): 

383 example_output[key] = value[i] 

384 elif isinstance(value, list): 384 ↛ 387line 384 didn't jump to line 387 because the condition on line 384 was always true

385 example_output[key] = value[i] 

386 else: 

387 example_output[key] = value 

388 results.append(example_output) 

389 

390 return results 

391 

392 

393def get_hook_name_for_layer(template: str, layer: int, **kwargs) -> str: 

394 """Generate a hook point name from a template. 

395 

396 Args: 

397 template: Hook name template with {layer} placeholder. 

398 layer: Layer index. 

399 **kwargs: Additional template parameters. 

400 

401 Returns: 

402 Formatted hook point name. 

403 """ 

404 return template.format(layer=layer, **kwargs) 

405 

406 

407def filter_cache_by_pattern( 

408 cache: Any, 

409 pattern: str, 

410) -> Dict[str, torch.Tensor]: 

411 """Filter activation cache entries by hook name pattern. 

412 

413 Args: 

414 cache: TransformerLens ActivationCache. 

415 pattern: Pattern to match (e.g., "attn.hook_pattern" will match 

416 all attention pattern hooks). 

417 

418 Returns: 

419 Dictionary of matching cache entries. 

420 """ 

421 return {name: value for name, value in cache.items() if pattern in name} 

422 

423 

424def get_model_info(model: Any) -> Dict[str, Any]: 

425 """Extract relevant model information for LIT display. 

426 

427 Args: 

428 model: HookedTransformer model. 

429 

430 Returns: 

431 Dictionary with model metadata. 

432 """ 

433 cfg = model.cfg 

434 return { 

435 "model_name": cfg.model_name, 

436 "n_layers": cfg.n_layers, 

437 "n_heads": cfg.n_heads, 

438 "d_model": cfg.d_model, 

439 "d_head": cfg.d_head, 

440 "d_mlp": cfg.d_mlp, 

441 "d_vocab": cfg.d_vocab, 

442 "n_ctx": cfg.n_ctx, 

443 "act_fn": cfg.act_fn, 

444 "normalization_type": cfg.normalization_type, 

445 "positional_embedding_type": cfg.positional_embedding_type, 

446 }