Coverage for transformer_lens/ActivationCache.py: 94%

466 statements  

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

1"""Activation Cache. 

2 

3The :class:`ActivationCache` is at the core of Transformer Lens. It is a wrapper that stores all 

4important activations from a forward pass of the model, and provides a variety of helper functions 

5to investigate them. 

6 

7Getting Started: 

8 

9When reading these docs for the first time, we recommend reading the main :class:`ActivationCache` 

10class first, including the examples, and then skimming the available methods. You can then refer 

11back to these docs depending on what you need to do. 

12""" 

13 

14from __future__ import annotations 

15 

16import logging 

17from collections import Counter 

18from typing import ( 

19 TYPE_CHECKING, 

20 Any, 

21 Callable, 

22 Dict, 

23 Iterator, 

24 List, 

25 Optional, 

26 Tuple, 

27 Union, 

28 cast, 

29) 

30 

31import einops 

32import numpy as np 

33import torch 

34from jaxtyping import Float, Int 

35from typing_extensions import Literal 

36 

37import transformer_lens.utilities as utils 

38from transformer_lens.utilities import Slice, SliceInput, warn_if_mps 

39 

40if TYPE_CHECKING: 

41 from transformer_lens.model_protocol import TransformerLensModelWithWeights 

42 

43 

44def _normalize_projection_to_2d( 

45 project: Optional[torch.Tensor], 

46) -> Tuple[Optional[torch.Tensor], bool]: 

47 """Return ``(project_2d, squeeze_at_end)`` — 1D projections are reshaped to 2D for uniform internal handling and squeezed back at the user-facing return.""" 

48 if project is None: 

49 return None, False 

50 if project.ndim == 1: 

51 return project.unsqueeze(-1), True 

52 return project, False 

53 

54 

55class ActivationCache: 

56 """Activation Cache. 

57 

58 A wrapper that stores all important activations from a forward pass of the model, and provides a 

59 variety of helper functions to investigate them. 

60 

61 The :class:`ActivationCache` is at the core of Transformer Lens. It is a wrapper that stores all 

62 important activations from a forward pass of the model, and provides a variety of helper 

63 functions to investigate them. The common way to access it is to run the model with 

64 :meth:`transformer_lens.model_bridge.TransformerBridge.run_with_cache`. 

65 

66 Examples: 

67 

68 When investigating a particular behaviour of a model, a very common first step is to try and 

69 understand which components of the model are most responsible for that behaviour. For example, 

70 if you're investigating the prompt "Why did the chicken cross the" -> " road", you might want to 

71 understand if there is a specific sublayer (mlp or multi-head attention) that is responsible for 

72 the model predicting "road". This kind of analysis commonly falls under the category of "logit 

73 attribution" or "direct logit attribution" (DLA). 

74 

75 >>> from transformer_lens.model_bridge import TransformerBridge 

76 >>> model = TransformerBridge.boot_transformers("roneneldan/TinyStories-1M") 

77 >>> model.enable_compatibility_mode() 

78 

79 >>> _logits, cache = model.run_with_cache("Why did the chicken cross the") 

80 >>> residual_stream, labels = cache.decompose_resid(return_labels=True, mode="attn") 

81 >>> print(labels[0:3]) 

82 ['embed', 'pos_embed', '0_attn_out'] 

83 

84 >>> answer = " road" # Note the proceeding space to match the model's tokenization 

85 >>> logit_attrs = cache.logit_attrs(residual_stream, answer) 

86 >>> print(logit_attrs.shape) # Attention layers 

87 torch.Size([10, 1, 7]) 

88 

89 >>> most_important_component_idx = torch.argmax(logit_attrs) 

90 >>> print(labels[most_important_component_idx]) 

91 3_attn_out 

92 

93 You can also dig in with more granularity, using :meth:`get_full_resid_decomposition` to get the 

94 residual stream by individual component (mlp neurons and individual attention heads). This 

95 creates a larger residual stack, but the approach of using :meth"`logit_attrs` remains the same. 

96 

97 Equally you might want to find out if the model struggles to construct such excellent jokes 

98 until the very last layers, or if it is trivial and the first few layers are enough. This kind 

99 of analysis is called "logit lens", and you can find out more about how to do that with 

100 :meth:`ActivationCache.accumulated_resid`. 

101 

102 Warning: 

103 

104 :class:`ActivationCache` is designed to be used with 

105 :class:`transformer_lens.model_bridge.TransformerBridge`. Advanced helpers expect the model to 

106 expose the TransformerLens weight-processing interface and generally expect a complete cache; 

107 some internal methods may break with other models or partial caches. 

108 

109 The biggest footgun and source of bugs in this code will be keeping track of indexes, 

110 dimensions, and the numbers of each. There are several kinds of activations: 

111 

112 * Internal attn head vectors: q, k, v, z. Shape [batch, pos, head_index, d_head]. 

113 * Internal attn pattern style results: pattern (post softmax), attn_scores (pre-softmax). Shape 

114 [batch, head_index, query_pos, key_pos]. 

115 * Attn head results: result. Shape [batch, pos, head_index, d_model]. 

116 * Internal MLP vectors: pre, post, mid (only used for solu_ln - the part between activation + 

117 layernorm). Shape [batch, pos, d_mlp]. 

118 * Residual stream vectors: resid_pre, resid_mid, resid_post, attn_out, mlp_out, embed, 

119 pos_embed, normalized (output of each LN or LNPre). Shape [batch, pos, d_model]. 

120 * LayerNorm Scale: scale. Shape [batch, pos, 1]. 

121 

122 Sometimes the batch dimension will be missing because we applied `remove_batch_dim` (used when 

123 batch_size=1), and as such all library functions *should* be robust to that. 

124 

125 Type annotations are in the following form: 

126 

127 * layers_covered is the number of layers queried in functions that stack the residual stream. 

128 * batch_and_pos_dims is the set of dimensions from batch and pos - by default this is ["batch", 

129 "pos"], but is only ["pos"] if we've removed the batch dimension and is [()] if we've removed 

130 batch dimension and are applying a pos slice which indexes a specific position. 

131 

132 Args: 

133 cache_dict: 

134 A dictionary of cached activations from a model run. 

135 model: 

136 The model that the activations are from. 

137 has_batch_dim: 

138 Whether the activations have a batch dimension. 

139 """ 

140 

141 def __init__( 

142 self, 

143 cache_dict: Dict[str, torch.Tensor], 

144 model: Any, 

145 has_batch_dim: bool = True, 

146 ): 

147 self.cache_dict = cache_dict 

148 # Advanced helpers (LN folding, residual-direction projection) need the 

149 # weight-processing surface, which TransformerBridge exposes. 

150 self.model = cast("TransformerLensModelWithWeights", model) 

151 self.has_batch_dim = has_batch_dim 

152 self.has_embed = "hook_embed" in self.cache_dict 

153 self.has_pos_embed = "hook_pos_embed" in self.cache_dict 

154 

155 # Note: model reference prevents garbage collection. Set cache.model = None if unneeded. 

156 

157 def _batch_size(self) -> int: 

158 """The cache's batch size: the most common leading dim across entries. 

159 

160 Caches may hold non-batch entries alongside genuinely batched 

161 activations — broadcast entries with a leading dim of 1 (e.g. the 

162 bridge's position-index inputs) or position-indexed entries whose 

163 leading dim is the sequence length (e.g. T5's relative position bias). 

164 The batched activations vastly outnumber both, so the mode is the 

165 reliable signal where max/min are not. 

166 """ 

167 counts = Counter(v.size(0) for v in self.cache_dict.values() if v.ndim > 0) 

168 return counts.most_common(1)[0][0] if counts else 1 

169 

170 def remove_batch_dim(self) -> ActivationCache: 

171 """Remove the Batch Dimension (if a single batch item). 

172 

173 Returns: 

174 The ActivationCache with the batch dimension removed. 

175 """ 

176 if self.has_batch_dim: 

177 batch_size = self._batch_size() 

178 assert ( 

179 batch_size == 1 

180 ), f"Cannot remove batch dimension from cache with batch size {batch_size}" 

181 for key in self.cache_dict: 

182 if self.cache_dict[key].ndim > 0 and self.cache_dict[key].size(0) == 1: 

183 self.cache_dict[key] = self.cache_dict[key][0] 

184 self.has_batch_dim = False 

185 else: 

186 logging.warning("Tried removing batch dimension after already having removed it.") 

187 return self 

188 

189 def __repr__(self) -> str: 

190 """Representation of the ActivationCache. 

191 

192 Special method that returns a string representation of an object. It's normally used to give 

193 a string that can be used to recreate the object, but here we just return a string that 

194 describes the object. 

195 """ 

196 return f"ActivationCache with keys {list(self.cache_dict.keys())}" 

197 

198 def __getitem__(self, key) -> torch.Tensor: 

199 """Retrieve Cached Activations by Key or Shorthand. 

200 

201 Enables direct access to cached activations via dictionary-style indexing using keys or 

202 shorthand naming conventions. 

203 

204 It also supports tuples for advanced indexing, with the dimension order as (name, layer_index, layer_type). 

205 See :func:`transformer_lens.utilities.get_act_name` for how shorthand is converted to a full name. 

206 

207 

208 Args: 

209 key: 

210 The key or shorthand name for the activation to retrieve. 

211 

212 Returns: 

213 The cached activation tensor corresponding to the given key. 

214 """ 

215 if key in self.cache_dict: 

216 return self.cache_dict[key] 

217 elif type(key) == str: 

218 return self.cache_dict[utils.get_act_name(key)] 

219 else: 

220 if len(key) > 1 and key[1] is not None: 

221 if key[1] < 0: 

222 # Supports negative indexing on the layer dimension 

223 key = (key[0], self.model.cfg.n_layers + key[1], *key[2:]) 

224 return self.cache_dict[utils.get_act_name(*key)] 

225 

226 def __len__(self) -> int: 

227 """Length of the ActivationCache. 

228 

229 Special method that returns the length of an object (in this case the number of different 

230 activations in the cache). 

231 """ 

232 return len(self.cache_dict) 

233 

234 def to(self, device: Union[str, torch.device]) -> ActivationCache: 

235 """Move the Cache to a Device. 

236 

237 Mostly useful for moving the cache to the CPU after model computation finishes to save GPU 

238 memory. Note however that operations will be much slower on the CPU. Note also that some 

239 methods will break unless the model is also moved to the same device, eg 

240 `compute_head_results`. 

241 

242 Args: 

243 device: 

244 The device to move the cache to (e.g. `torch.device.cpu`). 

245 

246 """ 

247 warn_if_mps(device) 

248 self.cache_dict = {key: value.to(device) for key, value in self.cache_dict.items()} 

249 return self 

250 

251 def toggle_autodiff(self, mode: bool = False): 

252 """Toggle Autodiff Globally. 

253 

254 Applies `torch.set_grad_enabled(mode)` to the global state (not just TransformerLens). 

255 

256 Warning: 

257 

258 This is pretty dangerous, since autodiff is global state - this turns off torch's 

259 ability to take gradients completely and it's easy to get a bunch of errors if you don't 

260 realise what you're doing. 

261 

262 But autodiff consumes a LOT of GPU memory (since every intermediate activation is cached 

263 until all downstream activations are deleted - this means that computing the loss and 

264 storing it in a list will keep every activation sticking around!). So often when you're 

265 analysing a model's activations, and don't need to do any training, autodiff is more trouble 

266 than its worth. 

267 

268 If you don't want to mess with global state, using torch.inference_mode as a context manager 

269 or decorator achieves similar effects: 

270 

271 >>> with torch.inference_mode(): 

272 ... y = torch.Tensor([1., 2, 3]) 

273 >>> y.requires_grad 

274 False 

275 """ 

276 logging.warning("Changed the global state, set autodiff to %s", mode) 

277 torch.set_grad_enabled(mode) 

278 

279 def keys(self): 

280 """Keys of the ActivationCache. 

281 

282 Examples: 

283 

284 >>> from transformer_lens.model_bridge import TransformerBridge 

285 >>> model = TransformerBridge.boot_transformers("roneneldan/TinyStories-1M") 

286 >>> model.enable_compatibility_mode() 

287 >>> _logits, cache = model.run_with_cache("Some prompt") 

288 >>> list(cache.keys())[0:8] 

289 ['embed.hook_in', 'hook_embed', 'embed.hook_out', 'pos_embed.hook_in', 

290 'hook_pos_embed', 'pos_embed.hook_out', 'blocks.0.hook_in', 

291 'blocks.0.hook_resid_pre'] 

292 

293 Returns: 

294 List of all keys. 

295 """ 

296 return self.cache_dict.keys() 

297 

298 def values(self): 

299 """Values of the ActivationCache. 

300 

301 Returns: 

302 List of all values. 

303 """ 

304 return self.cache_dict.values() 

305 

306 def items(self): 

307 """Items of the ActivationCache. 

308 

309 Returns: 

310 List of all items ((key, value) tuples). 

311 """ 

312 return self.cache_dict.items() 

313 

314 def __iter__(self) -> Iterator[str]: 

315 """ActivationCache Iterator. 

316 

317 Special method that returns an iterator over the keys in the ActivationCache. Allows looping over the 

318 cache. 

319 

320 Examples: 

321 

322 >>> from transformer_lens.model_bridge import TransformerBridge 

323 >>> model = TransformerBridge.boot_transformers("roneneldan/TinyStories-1M") 

324 >>> model.enable_compatibility_mode() 

325 >>> _logits, cache = model.run_with_cache("Some prompt") 

326 >>> cache_interesting_names = [] 

327 >>> for key in cache: 

328 ... if not key.startswith("blocks.") or key.startswith("blocks.0"): 

329 ... cache_interesting_names.append(key) 

330 >>> print(cache_interesting_names[0:8]) 

331 ['embed.hook_in', 'hook_embed', 'embed.hook_out', 'pos_embed.hook_in', 

332 'hook_pos_embed', 'pos_embed.hook_out', 'blocks.0.hook_in', 

333 'blocks.0.hook_resid_pre'] 

334 

335 Returns: 

336 Iterator over the cache. 

337 """ 

338 return self.cache_dict.__iter__() 

339 

340 def apply_slice_to_batch_dim(self, batch_slice: Union[Slice, SliceInput]) -> ActivationCache: 

341 """Apply a Slice to the Batch Dimension. 

342 

343 Args: 

344 batch_slice: 

345 The slice to apply to the batch dimension. 

346 

347 Returns: 

348 The ActivationCache with the batch dimension sliced. 

349 """ 

350 if not isinstance(batch_slice, Slice): 

351 batch_slice = Slice(batch_slice) 

352 batch_slice = cast(Slice, batch_slice) # mypy can't seem to infer this 

353 assert ( 

354 self.has_batch_dim or batch_slice.mode == "empty" 

355 ), "Cannot index into a cache without a batch dim" 

356 still_has_batch_dim = (batch_slice.mode != "int") and self.has_batch_dim 

357 batch_size = self._batch_size() 

358 # Broadcast entries (leading dim 1 when the true batch is larger) are not 

359 # batched — leave them untouched so slicing can't index out of bounds. 

360 new_cache_dict = { 

361 name: ( 

362 batch_slice.apply(param, dim=0) 

363 if param.ndim > 0 and param.size(0) == batch_size 

364 else param 

365 ) 

366 for name, param in self.cache_dict.items() 

367 } 

368 return ActivationCache(new_cache_dict, self.model, has_batch_dim=still_has_batch_dim) 

369 

370 def accumulated_resid( 

371 self, 

372 layer: Optional[int] = None, 

373 incl_mid: bool = False, 

374 apply_ln: bool = False, 

375 pos_slice: Optional[Union[Slice, SliceInput]] = None, 

376 mlp_input: bool = False, 

377 return_labels: bool = False, 

378 ) -> Union[ 

379 Float[torch.Tensor, "layers_covered *batch_and_pos_dims d_model"], 

380 Tuple[Float[torch.Tensor, "layers_covered *batch_and_pos_dims d_model"], List[str]], 

381 ]: 

382 """Accumulated Residual Stream. 

383 

384 Returns the accumulated residual stream at each layer/sub-layer. This is useful for `Logit 

385 Lens <https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens>` 

386 style analysis, where it can be thought of as what the model "believes" at each point in the 

387 residual stream. 

388 

389 To project this into the vocabulary space, remember that there is a final layer norm in most 

390 decoder-only transformers. Therefore, you need to first apply the final layer norm (which 

391 can be done with `apply_ln`), and then multiply by the unembedding matrix (:math:`W_U`) 

392 and optionally add the unembedding bias (:math:`b_U`). 

393 

394 **Note on bias terms:** There are two valid approaches for the final projection: 

395 

396 1. **With bias terms:** Use `model.unembed(normalized_resid)` which applies both :math:`W_U` 

397 and :math:`b_U` (equivalent to `normalized_resid @ model.W_U + model.b_U`). This works 

398 correctly with both `fold_ln=True` and `fold_ln=False` settings, as the biases are 

399 handled consistently. 

400 2. **Without bias terms:** Use only `normalized_resid @ model.W_U`. If taking this approach, 

401 you should instantiate the model with `fold_ln=True`, which folds the layer norm scaling 

402 into :math:`W_U` and the layer norm bias into :math:`b_U`. Since `apply_ln=True` will 

403 apply the (now parameter-free) layer norm, and you skip :math:`b_U`, no bias terms are 

404 included. With `fold_ln=False`, the layer norm bias would still be applied, which is 

405 typically not desired when excluding bias terms. 

406 

407 Both approaches are commonly used in the literature and are valid interpretability choices. 

408 

409 If you instead want to look at contributions to the residual stream from each component 

410 (e.g. for direct logit attribution), see :meth:`decompose_resid` instead, or 

411 :meth:`get_full_resid_decomposition` if you want contributions broken down further into each 

412 MLP neuron. 

413 

414 Examples: 

415 

416 Logit Lens analysis can be done as follows: 

417 

418 >>> from transformer_lens.model_bridge import TransformerBridge 

419 >>> import torch 

420 >>> import pandas as pd 

421 

422 >>> model = TransformerBridge.boot_transformers("roneneldan/TinyStories-1M", device="cpu") 

423 >>> model.enable_compatibility_mode() 

424 

425 >>> prompt = "Why did the chicken cross the" 

426 >>> answer = " road" 

427 >>> logits, cache = model.run_with_cache("Why did the chicken cross the") 

428 >>> answer_token = model.to_single_token(answer) 

429 >>> print(answer_token) 

430 2975 

431 

432 >>> accum_resid, labels = cache.accumulated_resid(return_labels=True, apply_ln=True) 

433 >>> last_token_accum = accum_resid[:, 0, -1, :] # layer, batch, pos, d_model 

434 >>> print(last_token_accum.shape) # layer, d_model 

435 torch.Size([9, 64]) 

436 

437 

438 >>> W_U = model.W_U 

439 >>> print(W_U.shape) 

440 torch.Size([64, 50257]) 

441 

442 >>> # Project to vocabulary without unembedding bias 

443 >>> layers_logits = last_token_accum @ W_U # layer, d_vocab 

444 >>> print(layers_logits.shape) 

445 torch.Size([9, 50257]) 

446 

447 >>> # The unembedding bias can be added on top when the model carries one 

448 >>> # (the rank table below stays on the bias-free logits): 

449 >>> b_U = getattr(model, "b_U", None) 

450 >>> with_bias = layers_logits + b_U if b_U is not None else layers_logits 

451 >>> print(with_bias.shape) 

452 torch.Size([9, 50257]) 

453 

454 >>> # Get the rank of the correct answer by layer 

455 >>> sorted_indices = torch.argsort(layers_logits, dim=1, descending=True) 

456 >>> rank_answer = (sorted_indices == 2975).nonzero(as_tuple=True)[1] 

457 >>> print(pd.Series(rank_answer, index=labels)) 

458 0_pre 4442 

459 1_pre 382 

460 2_pre 982 

461 3_pre 1160 

462 4_pre 408 

463 5_pre 145 

464 6_pre 78 

465 7_pre 387 

466 final_post 6 

467 dtype: int64 

468 

469 Args: 

470 layer: 

471 The layer to take components up to - by default includes resid_pre for that layer 

472 and excludes resid_mid and resid_post for that layer. If set as `n_layers`, `-1` or 

473 `None` it will return all residual streams, including the final one (i.e. 

474 immediately pre logits). The indices are taken such that this gives the accumulated 

475 streams up to the input to layer l. 

476 incl_mid: 

477 Whether to return `resid_mid` for all previous layers. 

478 apply_ln: 

479 Whether to apply the final layer norm to the stack. When True, applies 

480 `model.ln_final`, which recomputes normalization statistics (mean and 

481 variance/RMS) for each intermediate state in the stack, transforming the 

482 activations into the format expected by the unembedding layer. 

483 pos_slice: 

484 A slice object to apply to the pos dimension. Defaults to None, do nothing. 

485 mlp_input: 

486 Whether to include resid_mid for the current layer. This essentially gives the MLP 

487 input rather than the attention input. 

488 return_labels: 

489 Whether to return a list of labels for the residual stream components. Useful for 

490 labelling graphs. 

491 

492 Returns: 

493 A tensor of the accumulated residual streams. If `return_labels` is True, also returns a 

494 list of labels for the components (as a tuple in the form `(components, labels)`). 

495 """ 

496 if not isinstance(pos_slice, Slice): 

497 pos_slice = Slice(pos_slice) 

498 if layer is None or layer == -1: 

499 # Default to the residual stream immediately pre unembed 

500 layer = self.model.cfg.n_layers 

501 assert isinstance(layer, int) 

502 labels = [] 

503 components_list = [] 

504 for l in range(layer + 1): 

505 if l == self.model.cfg.n_layers: 

506 components_list.append(self[("resid_post", self.model.cfg.n_layers - 1)]) 

507 labels.append("final_post") 

508 continue 

509 components_list.append(self[("resid_pre", l)]) 

510 labels.append(f"{l}_pre") 

511 if (incl_mid and l < layer) or (mlp_input and l == layer): 

512 components_list.append(self[("resid_mid", l)]) 

513 labels.append(f"{l}_mid") 

514 components_list = [pos_slice.apply(c, dim=-2) for c in components_list] 

515 components = torch.stack(components_list, dim=0) 

516 if apply_ln: 

517 recompute_ln = layer == self.model.cfg.n_layers 

518 components = self.apply_ln_to_stack( 

519 components, 

520 layer, 

521 pos_slice=pos_slice, 

522 mlp_input=mlp_input, 

523 has_batch_dim=self.has_batch_dim, 

524 recompute_ln=recompute_ln, 

525 ) 

526 if return_labels: 

527 return components, labels 

528 else: 

529 return components 

530 

531 def logit_attrs( 

532 self, 

533 residual_stack: Float[torch.Tensor, "num_components *batch_and_pos_dims d_model"], 

534 tokens: Union[ 

535 str, 

536 int, 

537 Int[torch.Tensor, ""], 

538 Int[torch.Tensor, "batch"], 

539 Int[torch.Tensor, "batch position"], 

540 ], 

541 incorrect_tokens: Optional[ 

542 Union[ 

543 str, 

544 int, 

545 Int[torch.Tensor, ""], 

546 Int[torch.Tensor, "batch"], 

547 Int[torch.Tensor, "batch position"], 

548 ] 

549 ] = None, 

550 pos_slice: Union[Slice, SliceInput] = None, 

551 batch_slice: Union[Slice, SliceInput] = None, 

552 has_batch_dim: bool = True, 

553 ) -> Float[torch.Tensor, "num_components *batch_and_pos_dims_out"]: 

554 """Logit Attributions. 

555 

556 Takes a residual stack (typically the residual stream decomposed by components), and 

557 calculates how much each item in the stack "contributes" to specific tokens. 

558 

559 It does this by: 

560 1. Getting the residual directions of the tokens (i.e. reversing the unembed) 

561 2. Taking the dot product of each item in the residual stack, with the token residual 

562 directions. 

563 

564 Note that if incorrect tokens are provided, it instead takes the difference between the 

565 correct and incorrect tokens (to calculate the residual directions). This is useful as 

566 sometimes we want to know e.g. which components are most responsible for selecting the 

567 correct token rather than an incorrect one. For example in the `Interpretability in the Wild 

568 paper <https://arxiv.org/abs/2211.00593>` prompts such as "John and Mary went to the shops, 

569 John gave a bag to" were investigated, and it was therefore useful to calculate attribution 

570 for the :math:`\\text{Mary} - \\text{John}` residual direction. 

571 

572 Warning: 

573 

574 Choosing the correct `tokens` and `incorrect_tokens` is both important and difficult. When 

575 investigating specific components it's also useful to look at it's impact on all tokens 

576 (i.e. :math:`\\text{final_ln}(\\text{residual_stack_item}) W_U`). 

577 

578 Args: 

579 residual_stack: 

580 Stack of components of residual stream to get logit attributions for. 

581 tokens: 

582 Tokens to compute logit attributions on. 

583 incorrect_tokens: 

584 If provided, compute attributions on logit difference between tokens and 

585 incorrect_tokens. Must have the same shape as tokens. 

586 pos_slice: 

587 The slice to apply layer norm scaling on. Defaults to None, do nothing. 

588 batch_slice: 

589 The slice to take on the batch dimension during layer norm scaling. Defaults to 

590 None, do nothing. 

591 has_batch_dim: 

592 Whether residual_stack has a batch dimension. Defaults to True. 

593 

594 Returns: 

595 A tensor of the logit attributions or logit difference attributions if incorrect_tokens 

596 was provided. 

597 """ 

598 if not isinstance(pos_slice, Slice): 

599 pos_slice = Slice(pos_slice) 

600 

601 if not isinstance(batch_slice, Slice): 

602 batch_slice = Slice(batch_slice) 

603 

604 # Convert tokens to tensor for shape checking, but pass original to tokens_to_residual_directions 

605 tokens_for_shape_check = tokens 

606 

607 if isinstance(tokens_for_shape_check, str): 

608 tokens_for_shape_check = torch.as_tensor( 

609 self.model.to_single_token(tokens_for_shape_check) 

610 ) 

611 elif isinstance(tokens_for_shape_check, int): 

612 tokens_for_shape_check = torch.as_tensor(tokens_for_shape_check) 

613 

614 logit_directions = self.model.tokens_to_residual_directions(tokens) 

615 

616 if incorrect_tokens is not None: 

617 # Convert incorrect_tokens to tensor for shape checking, but pass original to tokens_to_residual_directions 

618 incorrect_tokens_for_shape_check = incorrect_tokens 

619 

620 if isinstance(incorrect_tokens_for_shape_check, str): 

621 incorrect_tokens_for_shape_check = torch.as_tensor( 

622 self.model.to_single_token(incorrect_tokens_for_shape_check) 

623 ) 

624 elif isinstance(incorrect_tokens_for_shape_check, int): 

625 incorrect_tokens_for_shape_check = torch.as_tensor(incorrect_tokens_for_shape_check) 

626 

627 if tokens_for_shape_check.shape != incorrect_tokens_for_shape_check.shape: 

628 raise ValueError( 

629 f"tokens and incorrect_tokens must have the same shape! \ 

630 (tokens.shape={tokens_for_shape_check.shape}, \ 

631 incorrect_tokens.shape={incorrect_tokens_for_shape_check.shape})" 

632 ) 

633 

634 # If incorrect_tokens was provided, take the logit difference 

635 logit_directions = logit_directions - self.model.tokens_to_residual_directions( 

636 incorrect_tokens 

637 ) 

638 

639 scaled_residual_stack = self.apply_ln_to_stack( 

640 residual_stack, 

641 layer=-1, 

642 pos_slice=pos_slice, 

643 batch_slice=batch_slice, 

644 has_batch_dim=has_batch_dim, 

645 ) 

646 

647 logit_attrs = (scaled_residual_stack * logit_directions).sum(dim=-1) 

648 return logit_attrs 

649 

650 def decompose_resid( 

651 self, 

652 layer: Optional[int] = None, 

653 mlp_input: bool = False, 

654 mode: Literal["all", "mlp", "attn"] = "all", 

655 apply_ln: bool = False, 

656 pos_slice: Union[Slice, SliceInput] = None, 

657 incl_embeds: bool = True, 

658 return_labels: bool = False, 

659 ) -> Union[ 

660 Float[torch.Tensor, "layers_covered *batch_and_pos_dims d_model"], 

661 Tuple[Float[torch.Tensor, "layers_covered *batch_and_pos_dims d_model"], List[str]], 

662 ]: 

663 """Decompose the Residual Stream. 

664 

665 Decomposes the residual stream input to layer L into a stack of the output of previous 

666 layers. The sum of these is the input to layer L (plus embedding and pos embedding). This is 

667 useful for attributing model behaviour to different components of the residual stream 

668 

669 Args: 

670 layer: 

671 The layer to take components up to - by default includes 

672 resid_pre for that layer and excludes resid_mid and resid_post for that layer. 

673 layer==n_layers means to return all layer outputs incl in the final layer, layer==0 

674 means just embed and pos_embed. The indices are taken such that this gives the 

675 accumulated streams up to the input to layer l 

676 mlp_input: 

677 Whether to include attn_out for the current 

678 layer - essentially decomposing the residual stream that's input to the MLP input 

679 rather than the Attn input. 

680 mode: 

681 Values are "all", "mlp" or "attn". "all" returns all 

682 components, "mlp" returns only the MLP components, and "attn" returns only the 

683 attention components. Defaults to "all". 

684 apply_ln: 

685 Whether to apply LayerNorm to the stack. 

686 pos_slice: 

687 A slice object to apply to the pos dimension. 

688 Defaults to None, do nothing. 

689 incl_embeds: 

690 Whether to include embed & pos_embed 

691 return_labels: 

692 Whether to return a list of labels for the residual stream components. 

693 Useful for labelling graphs. 

694 

695 Returns: 

696 A tensor of the accumulated residual streams. If `return_labels` is True, also returns 

697 a list of labels for the components (as a tuple in the form `(components, labels)`). 

698 """ 

699 if not isinstance(pos_slice, Slice): 

700 pos_slice = Slice(pos_slice) 

701 pos_slice = cast(Slice, pos_slice) # mypy can't seem to infer this 

702 if layer is None or layer == -1: 

703 # Default to the residual stream immediately pre unembed 

704 layer = self.model.cfg.n_layers 

705 assert isinstance(layer, int) 

706 

707 incl_attn = mode != "mlp" 

708 incl_mlp = mode != "attn" and not self.model.cfg.attn_only 

709 components_list = [] 

710 labels = [] 

711 if incl_embeds: 

712 if self.has_embed: 712 ↛ 715line 712 didn't jump to line 715 because the condition on line 712 was always true

713 components_list = [self["hook_embed"]] 

714 labels.append("embed") 

715 if self.has_pos_embed: 715 ↛ 719line 715 didn't jump to line 719 because the condition on line 715 was always true

716 components_list.append(self["hook_pos_embed"]) 

717 labels.append("pos_embed") 

718 

719 for l in range(layer): 

720 if incl_attn: 

721 components_list.append(self[("attn_out", l)]) 

722 labels.append(f"{l}_attn_out") 

723 if incl_mlp: 

724 components_list.append(self[("mlp_out", l)]) 

725 labels.append(f"{l}_mlp_out") 

726 if mlp_input and incl_attn: 

727 components_list.append(self[("attn_out", layer)]) 

728 labels.append(f"{layer}_attn_out") 

729 components_list = [pos_slice.apply(c, dim=-2) for c in components_list] 

730 components = torch.stack(components_list, dim=0) 

731 if apply_ln: 

732 components = self.apply_ln_to_stack( 

733 components, layer, pos_slice=pos_slice, mlp_input=mlp_input 

734 ) 

735 if return_labels: 

736 return components, labels 

737 else: 

738 return components 

739 

740 def compute_head_results( 

741 self, 

742 ): 

743 """Compute Head Results. 

744 

745 Computes and caches the results for each attention head, ie the amount contributed to the 

746 residual stream from that head. attn_out for a layer is the sum of head results plus b_O. 

747 Intended use is to enable use_attn_results when running and caching the model, but this can 

748 be useful if you forget. 

749 

750 TransformerBridge exposes ``blocks[i].attn.W_O`` via its component-mapping 

751 compatibility shim. 

752 """ 

753 # Return if valid 4D results exist; replace stale 3D Bridge entries if needed 

754 first_key = "blocks.0.attn.hook_result" 

755 if first_key in self.cache_dict: 

756 val = self.cache_dict[first_key] 

757 if isinstance(val, torch.Tensor) and val.ndim >= 4: 757 ↛ 761line 757 didn't jump to line 761 because the condition on line 757 was always true

758 logging.warning("Tried to compute head results when they were already cached") 

759 return 

760 # Remove stale 3D entries before recomputing 

761 for layer in range(self.model.cfg.n_layers): 

762 key = f"blocks.{layer}.attn.hook_result" 

763 if key in self.cache_dict: 

764 del self.cache_dict[key] 

765 for layer in range(self.model.cfg.n_layers): 

766 # Note that we haven't enabled set item on this object so we need to edit the underlying 

767 # cache_dict directly. 

768 

769 # Add singleton dimension to match W_O's shape for broadcasting 

770 z = einops.rearrange( 

771 self[("z", layer, "attn")], 

772 "... head_index d_head -> ... head_index d_head 1", 

773 ) 

774 

775 # Element-wise multiplication of z and W_O (with shape [head_index, d_head, d_model]) 

776 block = self.model.blocks[layer] 

777 result = z * block.attn.W_O 

778 

779 # Sum over d_head to get the contribution of each head to the residual stream 

780 self.cache_dict[f"blocks.{layer}.attn.hook_result"] = result.sum(dim=-2) 

781 

782 def ssm_layers(self, mixer_type: Optional[Union[type, Tuple[type, ...]]] = None) -> List[int]: 

783 """Return the block indices whose mixer is an SSM / recurrent mixer. 

784 

785 Family-agnostic and purely structural: finds each block's *realized* SSM 

786 mixer in its variant slot (``.mixer`` / ``.linear_attn`` / …) via 

787 ``find_ssm_mixer``, which excludes a hybrid's passthrough ``.mixer`` on 

788 non-SSM layers (e.g. NemotronH attention/MLP/MoE) — no dependence on 

789 ``cfg.layers_block_type``. 

790 

791 Args: 

792 mixer_type: Optional concrete bridge class to filter to (e.g. 

793 ``SSM2MixerBridge`` for only Mamba-2 layers). 

794 

795 Returns: 

796 Ascending list of SSM block indices. 

797 """ 

798 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

799 find_ssm_mixer, 

800 ) 

801 

802 bridge = self.model 

803 layers: List[int] = [] 

804 for i, block in enumerate(bridge.blocks): 

805 mixer = find_ssm_mixer(block) 

806 if mixer is None: 

807 continue 

808 if mixer_type is not None and not isinstance(mixer, mixer_type): 

809 continue 

810 layers.append(i) 

811 return layers 

812 

813 def _over_ssm_layers( 

814 self, 

815 fn: Callable[[Any, int], torch.Tensor], 

816 mixer_type: Optional[Union[type, Tuple[type, ...]]] = None, 

817 ) -> Union[torch.Tensor, Dict[int, torch.Tensor]]: 

818 """Apply ``fn(mixer, layer_idx)`` over every SSM layer; stack or dict. 

819 

820 The single SSM layer-enumeration used by both ``compute_ssm_state`` and 

821 ``compute_ssm_effective_attention``. Returns a stacked tensor (dim 0 = 

822 layer) when *every* block is an SSM layer, else a ``{layer_idx: result}`` 

823 dict over the SSM layers (heterogeneous hybrids). 

824 """ 

825 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

826 find_ssm_mixer, 

827 ) 

828 

829 bridge = self.model 

830 indices = self.ssm_layers(mixer_type=mixer_type) 

831 if not indices: 831 ↛ 832line 831 didn't jump to line 832 because the condition on line 831 was never true

832 raise RuntimeError( 

833 "No SSM mixer layers found. Use an SSM / hybrid bridge and " 

834 "run_with_cache first (gated-delta-net needs use_cache=False)." 

835 ) 

836 results: Dict[int, torch.Tensor] = { 

837 i: fn(cast(Any, find_ssm_mixer(bridge.blocks[i])), i) for i in indices 

838 } 

839 if indices == list(range(len(bridge.blocks))): 

840 return torch.stack([results[i] for i in indices], dim=0) 

841 return results 

842 

843 def compute_ssm_effective_attention( 

844 self, 

845 layer: Optional[int] = None, 

846 include_dt_scaling: bool = False, 

847 per_state_coord: bool = False, 

848 ) -> Union[torch.Tensor, Dict[int, torch.Tensor]]: 

849 """Materialize SSM effective attention for one or all SSM layers, any family. 

850 

851 The single discovery surface for effective attention across Mamba-1, 

852 Mamba-2, and gated-delta-net layers; dispatches to each layer's mixer 

853 ``compute_effective_attention`` regardless of family. Family-specific 

854 options are forwarded only to mixers whose signature accepts them. 

855 

856 Args: 

857 layer: Specific block index, or None for every SSM layer. 

858 include_dt_scaling: Forwarded to Mamba-1/Mamba-2 mixers (the 

859 reconstruction form); gated-delta-net does not accept it. 

860 per_state_coord: Forwarded to Mamba-1 only (per-state-coordinate 

861 matrices). Setting it True for another family raises ValueError. 

862 

863 Returns: 

864 A per-layer matrix for a single ``layer``; for ``layer=None`` a 

865 stacked tensor (dim 0 = layer) when every block is an SSM layer, else 

866 a ``{layer_idx: matrix}`` dict over the SSM layers. 

867 

868 Raises: 

869 TypeError: If the requested ``layer`` has no SSM mixer. 

870 ValueError: If an option is set that the target mixer does not support. 

871 RuntimeError: If ``layer=None`` finds no SSM layers. 

872 """ 

873 import inspect 

874 

875 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

876 find_ssm_mixer, 

877 ) 

878 

879 def _call(mixer: Any, layer_idx: int) -> torch.Tensor: 

880 fn = cast(Any, mixer).compute_effective_attention 

881 params = inspect.signature(fn).parameters 

882 kwargs: Dict[str, Any] = {} 

883 for name, value in ( 

884 ("include_dt_scaling", include_dt_scaling), 

885 ("per_state_coord", per_state_coord), 

886 ): 

887 if name in params: 

888 kwargs[name] = value 

889 elif value: 

890 raise ValueError( 

891 f"{type(mixer).__name__}.compute_effective_attention does not " 

892 f"support {name}=True." 

893 ) 

894 result: torch.Tensor = fn(cache=self, layer_idx=layer_idx, **kwargs) 

895 return result 

896 

897 if layer is not None: 

898 single = find_ssm_mixer(self.model.blocks[layer]) 

899 if single is None: 

900 raise TypeError(f"Block {layer} has no SSM mixer (no compute_effective_attention).") 

901 return _call(single, layer) 

902 return self._over_ssm_layers(_call) 

903 

904 def compute_ssm_state( 

905 self, 

906 layer: Optional[int] = None, 

907 time_step: Optional[int] = None, 

908 ) -> Union[torch.Tensor, Dict[int, torch.Tensor]]: 

909 """Reconstruct the recurrent SSM state ``S`` from this cache. 

910 

911 The single discovery surface for recurrent state across families — 

912 ``SSMMixerBridge`` (Mamba-1), ``SSM2MixerBridge`` (Mamba-2) and 

913 ``GatedDeltaNetBridge`` (gated delta rule) each reconstruct it — mirroring 

914 ``compute_head_results``. Read-only post-hoc reconstruction from cached 

915 hooks (no forward re-run); requires an SSM / SSM-hybrid bridge cached via 

916 ``run_with_cache`` (gated-delta-net additionally needs ``use_cache=False`` 

917 so its interior hooks fire). See the mixer's ``compute_ssm_state`` for the 

918 recurrence, shapes, and the ``time_step`` memory bound; state shape is 

919 family-specific, so ``layer=None`` returns a dict except when every block 

920 shares one mixer type. 

921 

922 Args: 

923 layer: Specific block index, or None for every SSM-state layer. 

924 time_step: Optional single position (memory-bounded); None for all. 

925 

926 Returns: 

927 A per-layer state tensor for a single ``layer``; for ``layer=None`` a 

928 stacked tensor (dim 0 = layer) when every block is an SSM-state layer, 

929 else a ``{layer_idx: state}`` dict over those layers. 

930 

931 Raises: 

932 TypeError: If the requested ``layer`` has no state-reconstructing mixer. 

933 RuntimeError: If ``layer=None`` finds no such layers. 

934 """ 

935 from transformer_lens.model_bridge.generalized_components import ( 

936 GatedDeltaNetBridge, 

937 SSM2MixerBridge, 

938 SSMMixerBridge, 

939 ) 

940 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

941 find_ssm_mixer, 

942 ) 

943 

944 # Every recurrent family reconstructs S_t from its cached interior hooks. 

945 state_mixers = (SSMMixerBridge, SSM2MixerBridge, GatedDeltaNetBridge) 

946 

947 def _call(mixer: Any, layer_idx: int) -> torch.Tensor: 

948 state: torch.Tensor = cast(Any, mixer).compute_ssm_state( 

949 self, layer_idx=layer_idx, time_step=time_step 

950 ) 

951 return state 

952 

953 if layer is not None: 

954 single = find_ssm_mixer(self.model.blocks[layer]) 

955 if not isinstance(single, state_mixers): 

956 raise TypeError( 

957 f"Block {layer} has no state-reconstructing SSM mixer; " 

958 "compute_ssm_state supports Mamba-1 / Mamba-2 / gated-delta-net." 

959 ) 

960 return _call(single, layer) 

961 return self._over_ssm_layers(_call, mixer_type=state_mixers) 

962 

963 def stack_head_results( 

964 self, 

965 layer: int = -1, 

966 return_labels: bool = False, 

967 incl_remainder: bool = False, 

968 pos_slice: Union[Slice, SliceInput] = None, 

969 apply_ln: bool = False, 

970 ) -> Union[ 

971 Float[torch.Tensor, "num_components *batch_and_pos_dims d_model"], 

972 Tuple[Float[torch.Tensor, "num_components *batch_and_pos_dims d_model"], List[str]], 

973 ]: 

974 """Stack Head Results. 

975 

976 Returns a stack of all head results (ie residual stream contribution) up to layer L. A good 

977 way to decompose the outputs of attention layers into attribution by specific heads. Note 

978 that the num_components axis has length layer x n_heads ((layer head_index) in einops 

979 notation). 

980 

981 Args: 

982 layer: 

983 Layer index - heads at all layers strictly before this are included. layer must be 

984 in [1, n_layers-1], or any of (n_layers, -1, None), which all mean the final layer. 

985 return_labels: 

986 Whether to also return a list of labels of the form "L0H0" for the heads. 

987 incl_remainder: 

988 Whether to return a final term which is "the rest of the residual stream". 

989 pos_slice: 

990 A slice object to apply to the pos dimension. Defaults to None, do nothing. 

991 apply_ln: 

992 Whether to apply LayerNorm to the stack. 

993 """ 

994 if not isinstance(pos_slice, Slice): 

995 pos_slice = Slice(pos_slice) 

996 pos_slice = cast(Slice, pos_slice) # mypy can't seem to infer this 

997 if layer is None or layer == -1: 

998 # Default to the residual stream immediately pre unembed 

999 layer = self.model.cfg.n_layers 

1000 

1001 # Idempotent; cleans up stale Bridge entries 

1002 self.compute_head_results() 

1003 

1004 components: Any = [] 

1005 labels = [] 

1006 for l in range(layer): 

1007 # Note that this has shape batch x pos x head_index x d_model 

1008 components.append(pos_slice.apply(self[("result", l, "attn")], dim=-3)) 

1009 labels.extend([f"L{l}H{h}" for h in range(self.model.cfg.n_heads)]) 

1010 if components: 

1011 components = torch.cat(components, dim=-2) 

1012 components = einops.rearrange( 

1013 components, 

1014 "... concat_head_index d_model -> concat_head_index ... d_model", 

1015 ) 

1016 if incl_remainder: 

1017 remainder = pos_slice.apply( 

1018 self[("resid_post", layer - 1)], dim=-2 

1019 ) - components.sum(dim=0) 

1020 components = torch.cat([components, remainder[None]], dim=0) 

1021 labels.append("remainder") 

1022 elif incl_remainder: 

1023 # There are no components, so the remainder is the entire thing. 

1024 components = torch.cat( 

1025 [pos_slice.apply(self[("resid_post", layer - 1)], dim=-2)[None]], dim=0 

1026 ) 

1027 labels.append("remainder") 

1028 else: 

1029 # If this is called with layer 0, we return an empty tensor of the right shape to be 

1030 # stacked correctly. This uses the shape of hook_embed, which is pretty janky since it 

1031 # assumes embed is in the cache. But it's hard to explicitly code the shape, since it 

1032 # depends on the pos slice, whether we have a batch dim, etc. And it's pretty messy! 

1033 components = torch.zeros( 

1034 0, 

1035 *pos_slice.apply(self["hook_embed"], dim=-2).shape, 

1036 device=self.model.cfg.device, 

1037 ) 

1038 

1039 if apply_ln: 

1040 components = self.apply_ln_to_stack(components, layer, pos_slice=pos_slice) 

1041 

1042 if return_labels: 

1043 return components, labels 

1044 else: 

1045 return components 

1046 

1047 def stack_activation( 

1048 self, 

1049 activation_name: str, 

1050 layer: int = -1, 

1051 sublayer_type: Optional[str] = None, 

1052 ) -> Float[torch.Tensor, "layers_covered ..."]: 

1053 """Stack Activations. 

1054 

1055 Flexible way to stack activations with a given name. 

1056 

1057 Args: 

1058 activation_name: 

1059 The name of the activation to be stacked 

1060 layer: 

1061 'Layer index - heads' at all layers strictly before this are included. layer must be 

1062 in [1, n_layers-1], or any of (n_layers, -1, None), which all mean the final layer. 

1063 sublayer_type: 

1064 The sub layer type of the activation, passed to utils.get_act_name. Can normally be 

1065 inferred. 

1066 incl_remainder: 

1067 Whether to return a final term which is "the rest of the residual stream". 

1068 """ 

1069 if layer is None or layer == -1: 

1070 # Default to the residual stream immediately pre unembed 

1071 layer = self.model.cfg.n_layers 

1072 

1073 components = [] 

1074 for l in range(layer): 

1075 components.append(self[(activation_name, l, sublayer_type)]) 

1076 

1077 return torch.stack(components, dim=0) 

1078 

1079 def get_neuron_results( 

1080 self, 

1081 layer: int, 

1082 neuron_slice: Union[Slice, SliceInput] = None, 

1083 pos_slice: Union[Slice, SliceInput] = None, 

1084 project_output_onto: Optional[torch.Tensor] = None, 

1085 ) -> torch.Tensor: 

1086 """Get Neuron Results. 

1087 

1088 Get the results of for neurons in a specific layer (i.e, how much each neuron contributes to 

1089 the residual stream). Does it for the subset of neurons specified by neuron_slice, defaults 

1090 to all of them. Does *not* cache these because it's expensive in space and cheap to compute. 

1091 

1092 Args: 

1093 layer: 

1094 Layer index. 

1095 neuron_slice: 

1096 Slice of the neuron. 

1097 pos_slice: 

1098 Slice of the positions. 

1099 project_output_onto: 

1100 Optional ``[d_model]`` or ``[d_model, num_outputs]`` projection. Contracted with 

1101 ``W_out`` *before* the per-neuron expansion so the ``[..., d_mlp, d_model]`` 

1102 intermediate is never materialized. 

1103 

1104 Returns: 

1105 Last-dim is ``d_model`` (default), ``num_outputs`` (2D projection), or squeezed 

1106 (1D projection). 

1107 """ 

1108 if not isinstance(neuron_slice, Slice): 

1109 neuron_slice = Slice(neuron_slice) 

1110 if not isinstance(pos_slice, Slice): 

1111 pos_slice = Slice(pos_slice) 

1112 

1113 neuron_acts = self[("post", layer, "mlp")] 

1114 block = self.model.blocks[layer] 

1115 W_out = block.mlp.W_out 

1116 if pos_slice is not None: 1116 ↛ 1120line 1116 didn't jump to line 1120 because the condition on line 1116 was always true

1117 # Note - order is important, as Slice.apply *may* collapse a dimension, so this ensures 

1118 # that position dimension is -2 when we apply position slice 

1119 neuron_acts = pos_slice.apply(neuron_acts, dim=-2) 

1120 if neuron_slice is not None: 1120 ↛ 1123line 1120 didn't jump to line 1123 because the condition on line 1120 was always true

1121 neuron_acts = neuron_slice.apply(neuron_acts, dim=-1) 

1122 W_out = neuron_slice.apply(W_out, dim=0) 

1123 if project_output_onto is None: 

1124 return neuron_acts[..., None] * W_out 

1125 # W_out: [d_mlp, d_model]; project: [d_model] or [d_model, n_outs] 

1126 projected = W_out @ project_output_onto 

1127 if projected.ndim == 1: 

1128 return neuron_acts * projected 

1129 return neuron_acts[..., None] * projected 

1130 

1131 def _get_cached_ln_scale( 

1132 self, 

1133 layer: Optional[int], 

1134 mlp_input: bool, 

1135 pos_slice: Slice, 

1136 batch_slice: Optional[Slice] = None, 

1137 ) -> torch.Tensor: 

1138 """Look up the cached LN scale and apply pos/batch slicing. Surfaces a clearer error 

1139 when the expected hook isn't in the cache (some non-decoder-only architectures expose 

1140 LN scale at a different path or not at all). 

1141 """ 

1142 if layer == self.model.cfg.n_layers or layer is None: 

1143 key = "ln_final.hook_scale" 

1144 else: 

1145 key = f"blocks.{layer}.ln{2 if mlp_input else 1}.hook_scale" 

1146 try: 

1147 scale = self[key] 

1148 except KeyError as e: 

1149 raise KeyError( 

1150 f"Cached LN scale not found at '{key}'. apply_ln operations require the model " 

1151 f"to have cached this hook (some non-decoder-only architectures expose LN scale " 

1152 f"under different module paths)." 

1153 ) from e 

1154 scale = pos_slice.apply(scale, dim=-2) 

1155 if batch_slice is not None and self.has_batch_dim: 

1156 scale = batch_slice.apply(scale) 

1157 return scale 

1158 

1159 def _stack_neuron_results_apply_ln_projected( 

1160 self, 

1161 layer: int, 

1162 pos_slice: Slice, 

1163 neuron_slice: Slice, 

1164 project_2d: torch.Tensor, 

1165 ) -> torch.Tensor: 

1166 """LN-applied neuron stack with projection folded in — no d_mlp×d_model intermediate. 

1167 

1168 Analytical formula (LN models, cached scale ``s``): 

1169 ``LN_s(a_n * W_out_n) @ p = (a_n / s) * (W_out_n @ p - mean(W_out_n) * sum_p)`` 

1170 RMS models drop the ``mean(W_out_n) * sum_p`` term (no centering). Always uses the 

1171 ln1 scale (mlp_input=False) since ``stack_neuron_results`` doesn't expose mlp_input. 

1172 

1173 """ 

1174 scale = self._get_cached_ln_scale(layer, mlp_input=False, pos_slice=pos_slice) 

1175 

1176 apply_centering = self.model.cfg.normalization_type in ["LN", "LNPre"] 

1177 sum_p = project_2d.sum(dim=0) if apply_centering else None # [n_outs] 

1178 

1179 components: list = [] 

1180 for l in range(layer): 

1181 block = self.model.blocks[l] 

1182 W_out_l = block.mlp.W_out # [d_mlp, d_model] 

1183 W_out_l_sliced = neuron_slice.apply(W_out_l, dim=0) 

1184 W_proj_l = W_out_l_sliced @ project_2d # [d_mlp, n_outs] 

1185 if apply_centering: 1185 ↛ 1190line 1185 didn't jump to line 1190 because the condition on line 1185 was always true

1186 assert sum_p is not None # set when apply_centering, narrow for mypy 

1187 W_means_l = W_out_l_sliced.mean(dim=-1) # [d_mlp] 

1188 lin_form_l = W_proj_l - W_means_l[:, None] * sum_p[None, :] 

1189 else: 

1190 lin_form_l = W_proj_l 

1191 a_l = self[("post", l, "mlp")] 

1192 a_l = pos_slice.apply(a_l, dim=-2) 

1193 a_l = neuron_slice.apply(a_l, dim=-1) 

1194 # (a_l / s)[..., None] is [..., d_mlp, 1]; broadcast with lin_form_l [d_mlp, n_outs] 

1195 components.append((a_l / scale)[..., None] * lin_form_l) 

1196 if not components: 1196 ↛ 1197line 1196 didn't jump to line 1197 because the condition on line 1196 was never true

1197 empty_src = pos_slice.apply(self["hook_embed"], dim=-2) 

1198 return torch.zeros( 

1199 0, *empty_src.shape[:-1], project_2d.shape[-1], device=self.model.cfg.device 

1200 ) 

1201 stacked = torch.cat(components, dim=-2) 

1202 return einops.rearrange( 

1203 stacked, "... concat_neuron_index n_outs -> concat_neuron_index ... n_outs" 

1204 ) 

1205 

1206 def stack_neuron_results( 

1207 self, 

1208 layer: int, 

1209 pos_slice: Union[Slice, SliceInput] = None, 

1210 neuron_slice: Union[Slice, SliceInput] = None, 

1211 return_labels: bool = False, 

1212 incl_remainder: bool = False, 

1213 apply_ln: bool = False, 

1214 project_output_onto: Optional[torch.Tensor] = None, 

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

1216 """Stack Neuron Results 

1217 

1218 Returns a stack of all neuron results (ie residual stream contribution) up to layer L - ie 

1219 the amount each individual neuron contributes to the residual stream. Also returns a list of 

1220 labels of the form "L0N0" for the neurons. A good way to decompose the outputs of MLP layers 

1221 into attribution by specific neurons. 

1222 

1223 Note that doing this for all neurons is SUPER expensive on GPU memory and only works for 

1224 small models or short inputs. Pass ``project_output_onto`` to fold the projection into the 

1225 per-neuron expansion and avoid the ``[..., d_mlp, d_model]`` intermediate. 

1226 

1227 Args: 

1228 layer: 

1229 Layer index - heads at all layers strictly before this are included. layer must be 

1230 in [1, n_layers] 

1231 pos_slice: 

1232 Slice of the positions. 

1233 neuron_slice: 

1234 Slice of the neurons. 

1235 return_labels: 

1236 Whether to also return a list of labels of the form "L0H0" for the heads. 

1237 incl_remainder: 

1238 Whether to return a final term which is "the rest of the residual stream". 

1239 apply_ln: 

1240 Whether to apply LayerNorm to the stack. 

1241 project_output_onto: 

1242 Optional ``[d_model]`` or ``[d_model, num_outputs]`` tensor. When set, each 

1243 component's last d_model dim is replaced by the projection (memory-efficient for 

1244 direction analyses; see ``get_neuron_results``). Combined with ``apply_ln=True``, 

1245 the projection is folded into the analytical cached-scale LN so the 

1246 ``[..., d_mlp, d_model]`` intermediate is still never materialized. 

1247 """ 

1248 if layer is None or layer == -1: 

1249 # Default to the residual stream immediately pre unembed 

1250 layer = self.model.cfg.n_layers 

1251 

1252 if not isinstance(neuron_slice, Slice): 

1253 neuron_slice = Slice(neuron_slice) 

1254 if not isinstance(pos_slice, Slice): 

1255 pos_slice = Slice(pos_slice) 

1256 

1257 project_2d, squeeze_projected = _normalize_projection_to_2d(project_output_onto) 

1258 

1259 d_mlp = self.model.cfg.d_mlp 

1260 assert d_mlp is not None, "model.cfg.d_mlp must be set" 

1261 neuron_labels: Union[torch.Tensor, np.ndarray] = neuron_slice.apply( 

1262 torch.arange(d_mlp), dim=0 

1263 ) 

1264 if isinstance(neuron_labels, int): 1264 ↛ 1265line 1264 didn't jump to line 1265 because the condition on line 1264 was never true

1265 neuron_labels = np.array([neuron_labels]) 

1266 

1267 labels = [f"L{l}N{h}" for l in range(layer) for h in neuron_labels] 

1268 components: Any 

1269 ln_folded = apply_ln and project_2d is not None 

1270 if ln_folded: 

1271 assert project_2d is not None # narrow for mypy 

1272 # Analytical LN+projection — no d_mlp×d_model intermediate. 

1273 components = self._stack_neuron_results_apply_ln_projected( 

1274 layer, pos_slice, neuron_slice, project_2d 

1275 ) 

1276 if incl_remainder: 

1277 # Linearity of cached-scale LN: remainder is LN_s(resid_post) @ p - sum(neurons). 

1278 resid_post = pos_slice.apply(self[("resid_post", layer - 1)], dim=-2) 

1279 resid_post_ln = self.apply_ln_to_stack( 

1280 resid_post[None], layer, pos_slice=pos_slice 

1281 )[0] 

1282 remainder = resid_post_ln @ project_2d 

1283 if components.shape[0] > 0: 1283 ↛ 1285line 1283 didn't jump to line 1285 because the condition on line 1283 was always true

1284 remainder = remainder - components.sum(dim=0) 

1285 components = torch.cat([components, remainder[None]], dim=0) 

1286 labels.append("remainder") 

1287 else: 

1288 per_layer: list = [] 

1289 for l in range(layer): 

1290 per_layer.append( 

1291 self.get_neuron_results( 

1292 l, 

1293 pos_slice=pos_slice, 

1294 neuron_slice=neuron_slice, 

1295 project_output_onto=project_2d, 

1296 ) 

1297 ) 

1298 if per_layer: 

1299 components = torch.cat(per_layer, dim=-2) 

1300 components = einops.rearrange( 

1301 components, 

1302 "... concat_neuron_index d_model -> concat_neuron_index ... d_model", 

1303 ) 

1304 if incl_remainder: 

1305 remainder_full = pos_slice.apply(self[("resid_post", layer - 1)], dim=-2) 

1306 if project_2d is not None: 

1307 remainder_full = remainder_full @ project_2d 

1308 remainder = remainder_full - components.sum(dim=0) 

1309 components = torch.cat([components, remainder[None]], dim=0) 

1310 labels.append("remainder") 

1311 elif incl_remainder: 

1312 remainder_full = pos_slice.apply(self[("resid_post", layer - 1)], dim=-2) 

1313 if project_2d is not None: 1313 ↛ 1314line 1313 didn't jump to line 1314 because the condition on line 1313 was never true

1314 remainder_full = remainder_full @ project_2d 

1315 components = torch.cat([remainder_full[None]], dim=0) 

1316 labels.append("remainder") 

1317 else: 

1318 empty_shape_src = pos_slice.apply(self["hook_embed"], dim=-2) 

1319 if project_2d is not None: 1319 ↛ 1320line 1319 didn't jump to line 1320 because the condition on line 1319 was never true

1320 empty_shape_src = empty_shape_src @ project_2d 

1321 components = torch.zeros(0, *empty_shape_src.shape, device=self.model.cfg.device) 

1322 

1323 if apply_ln: 

1324 components = self.apply_ln_to_stack(components, layer, pos_slice=pos_slice) 

1325 

1326 if squeeze_projected: 

1327 components = components.squeeze(-1) 

1328 

1329 if return_labels: 

1330 return components, labels 

1331 else: 

1332 return components 

1333 

1334 def apply_ln_to_stack( 

1335 self, 

1336 residual_stack: Float[torch.Tensor, "num_components *batch_and_pos_dims d_model"], 

1337 layer: Optional[int] = None, 

1338 mlp_input: bool = False, 

1339 pos_slice: Union[Slice, SliceInput] = None, 

1340 batch_slice: Union[Slice, SliceInput] = None, 

1341 has_batch_dim: bool = True, 

1342 recompute_ln: bool = False, 

1343 ) -> Float[torch.Tensor, "num_components *batch_and_pos_dims_out d_model"]: 

1344 """Apply Layer Norm to a Stack. 

1345 

1346 Takes a stack of components of the residual stream (eg outputs of decompose_resid or 

1347 accumulated_resid), treats them as the input to a specific layer, and applies the layer norm 

1348 scaling of that layer to them, using the cached scale factors - simulating what that 

1349 component of the residual stream contributes to that layer's input. 

1350 

1351 The layernorm scale is global across the entire residual stream for each layer, batch 

1352 element and position, which is why we need to use the cached scale factors rather than just 

1353 applying a new LayerNorm. 

1354 

1355 When recompute_ln=True and the target layer is the final layer (unembed), each 

1356 component is normalized using stats recomputed from that component; use this for logit lens 

1357 analysis. When recompute_ln=False, a single cached scale is used for all components. 

1358 

1359 If the model does not use LayerNorm or RMSNorm, it returns the residual stack unchanged. 

1360 

1361 Args: 

1362 residual_stack: 

1363 A tensor, whose final dimension is d_model. The other trailing dimensions are 

1364 assumed to be the same as the stored hook_scale - which may or may not include batch 

1365 or position dimensions. 

1366 layer: 

1367 The layer we're taking the input to. In [0, n_layers], n_layers means the unembed. 

1368 None maps to the n_layers case, ie the unembed. 

1369 mlp_input: 

1370 Whether the input is to the MLP or attn (ie ln2 vs ln1). Defaults to False, ie ln1. 

1371 If layer==n_layers, must be False, and we use ln_final 

1372 pos_slice: 

1373 The slice to take of positions, if residual_stack is not over the full context, None 

1374 means do nothing. It is assumed that pos_slice has already been applied to 

1375 residual_stack, and this is only applied to the scale. See utils.Slice for details. 

1376 Defaults to None, do nothing. 

1377 batch_slice: 

1378 The slice to take on the batch dimension. Defaults to None, do nothing. 

1379 has_batch_dim: 

1380 Whether residual_stack has a batch dimension. 

1381 recompute_ln: 

1382 If True and target layer is the unembed (final layer), apply the final layer norm 

1383 to each component with statistics recomputed from that component. Defaults to False. 

1384 

1385 """ 

1386 if self.model.cfg.normalization_type not in ["LN", "LNPre", "RMS", "RMSPre"]: 1386 ↛ 1388line 1386 didn't jump to line 1388 because the condition on line 1386 was never true

1387 # The model does not use LayerNorm, so we don't need to do anything. 

1388 return residual_stack 

1389 if not isinstance(pos_slice, Slice): 

1390 pos_slice = Slice(pos_slice) 

1391 if not isinstance(batch_slice, Slice): 

1392 batch_slice = Slice(batch_slice) 

1393 

1394 if layer is None or layer == -1: 

1395 # Default to the residual stream immediately pre unembed 

1396 layer = self.model.cfg.n_layers 

1397 

1398 if has_batch_dim: 

1399 # Apply batch slice to the stack 

1400 residual_stack = batch_slice.apply(residual_stack, dim=1) 

1401 

1402 # Logit lens: apply final layer norm to each component with recomputed statistics 

1403 if recompute_ln and layer == self.model.cfg.n_layers and hasattr(self.model, "ln_final"): 

1404 ln_final = self.model.ln_final 

1405 results = [] 

1406 for i in range(residual_stack.shape[0]): 

1407 x = residual_stack[i] 

1408 original_shape = x.shape 

1409 # ln_final expects (batch, pos, d_model); restore missing structural dimensions 

1410 if not has_batch_dim: 

1411 x = x.unsqueeze(0) 

1412 if x.ndim == 2: 

1413 x = x.unsqueeze(1) 

1414 out = ln_final(x) 

1415 results.append(out.reshape(original_shape)) 

1416 return torch.stack(results, dim=0) 

1417 

1418 # Center the stack onlny if the model uses LayerNorm 

1419 if self.model.cfg.normalization_type in ["LN", "LNPre"]: 

1420 residual_stack = residual_stack - residual_stack.mean(dim=-1, keepdim=True) 

1421 

1422 # Shape is [batch, position, 1] or [position, 1]; final dim is a dummy for broadcasting. 

1423 scale = self._get_cached_ln_scale(layer, mlp_input, pos_slice, batch_slice) 

1424 

1425 return residual_stack / scale 

1426 

1427 def get_full_resid_decomposition( 

1428 self, 

1429 layer: Optional[int] = None, 

1430 mlp_input: bool = False, 

1431 expand_neurons: bool = True, 

1432 apply_ln: bool = False, 

1433 pos_slice: Union[Slice, SliceInput] = None, 

1434 return_labels: bool = False, 

1435 project_output_onto: Optional[torch.Tensor] = None, 

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

1437 """Get the full Residual Decomposition. 

1438 

1439 Decomposes the residual stream that is input into some layer into its 

1440 constituent components: every attention head result, every neuron (or 

1441 MLP layer) result, the embeddings, and the accumulated biases. 

1442 

1443 The returned tensor stacks components along ``dim=0`` in this order: 

1444 

1445 1. Attention head results, layer-by-layer (``L * n_heads`` rows) 

1446 2. Neuron / MLP results (only if ``cfg.attn_only=False`` and 

1447 ``layer > 0``; ``L * d_mlp`` rows when ``expand_neurons=True``, 

1448 else ``L`` rows) 

1449 3. ``embed`` (1 row, if the model has token embeddings) 

1450 4. ``pos_embed`` (1 row, if the model has positional embeddings) 

1451 5. ``bias`` (1 row, the accumulated layer biases) 

1452 

1453 ``return_labels=True`` returns a list of strings in the same order, so 

1454 ``labels[i]`` always names ``stack[i]``. If you need to extract a 

1455 specific component, slice by label rather than by hard-coded index — 

1456 the row counts depend on ``layer``, ``expand_neurons``, 

1457 ``cfg.attn_only``, and whether the model has positional embeddings. 

1458 

1459 Args: 

1460 layer: 

1461 The layer we're inputting into. layer is in [0, n_layers], if layer==n_layers (or 

1462 None) we're inputting into the unembed (the entire stream), if layer==0 then it's 

1463 just embed and pos_embed 

1464 mlp_input: 

1465 Are we inputting to the MLP in that layer or the attn? Must be False for final 

1466 layer, since that's the unembed. 

1467 expand_neurons: 

1468 Whether to expand the MLP outputs to give every neuron's result or just return the 

1469 MLP layer outputs. 

1470 apply_ln: 

1471 Whether to apply LayerNorm to the stack. 

1472 pos_slice: 

1473 Slice of the positions to take. 

1474 return_labels: 

1475 Whether to return the labels. 

1476 project_output_onto: 

1477 Optional ``[d_model]`` or ``[d_model, num_outputs]`` projection. Folded in 

1478 *before* the per-neuron expansion, so the ``[..., d_mlp, d_model]`` intermediate 

1479 is never materialized (memory saving applies only with ``expand_neurons=True``). 

1480 Combined with ``apply_ln=True``, the projection is fused into the analytical 

1481 cached-scale LN so the same memory benefit holds. Output last-dim is squeezed 

1482 for a 1D projection; ``num_outputs`` for 2D. 

1483 """ 

1484 if layer is None or layer == -1: 

1485 # Default to the residual stream immediately pre unembed 

1486 layer = self.model.cfg.n_layers 

1487 assert layer is not None # keep mypy happy 

1488 

1489 if not isinstance(pos_slice, Slice): 

1490 pos_slice = Slice(pos_slice) 

1491 

1492 project_2d, squeeze_projected = _normalize_projection_to_2d(project_output_onto) 

1493 # When both apply_ln and projection are requested, LN is applied per-component (in 

1494 # d_model space for the small ones, analytically for neurons) before projection, so the 

1495 # final apply_ln_to_stack call is skipped — last-dim is already n_outs. 

1496 ln_folded = apply_ln and project_2d is not None 

1497 

1498 def _ln_then_project(stack: torch.Tensor) -> torch.Tensor: 

1499 stack = self.apply_ln_to_stack(stack, layer, pos_slice=pos_slice, mlp_input=mlp_input) 

1500 return stack @ project_2d if project_2d is not None else stack 

1501 

1502 head_stack, head_labels = self.stack_head_results( 

1503 layer + (1 if mlp_input else 0), pos_slice=pos_slice, return_labels=True 

1504 ) 

1505 if ln_folded: 

1506 head_stack = _ln_then_project(head_stack) 

1507 elif project_2d is not None: 

1508 head_stack = head_stack @ project_2d 

1509 labels = head_labels 

1510 components = [head_stack] 

1511 if not self.model.cfg.attn_only and layer > 0: 

1512 if expand_neurons: 

1513 # Only ask stack_neuron_results to apply LN when we want the fused analytical 

1514 # path (ln_folded). For the unfolded case the outer apply_ln_to_stack handles it. 

1515 neuron_stack, neuron_labels = self.stack_neuron_results( 

1516 layer, 

1517 pos_slice=pos_slice, 

1518 return_labels=True, 

1519 apply_ln=ln_folded, 

1520 project_output_onto=project_2d, 

1521 ) 

1522 labels.extend(neuron_labels) 

1523 components.append(neuron_stack) 

1524 else: 

1525 # Get the stack of just the MLP outputs 

1526 # mlp_input included for completeness, but it doesn't actually matter, since it's 

1527 # just for MLP outputs 

1528 mlp_stack, mlp_labels = self.decompose_resid( 

1529 layer, 

1530 mlp_input=mlp_input, 

1531 pos_slice=pos_slice, 

1532 incl_embeds=False, 

1533 mode="mlp", 

1534 return_labels=True, 

1535 ) 

1536 if ln_folded: 1536 ↛ 1537line 1536 didn't jump to line 1537 because the condition on line 1536 was never true

1537 mlp_stack = _ln_then_project(mlp_stack) 

1538 elif project_2d is not None: 1538 ↛ 1539line 1538 didn't jump to line 1539 because the condition on line 1538 was never true

1539 mlp_stack = mlp_stack @ project_2d 

1540 labels.extend(mlp_labels) 

1541 components.append(mlp_stack) 

1542 

1543 if self.has_embed: 1543 ↛ 1551line 1543 didn't jump to line 1551 because the condition on line 1543 was always true

1544 embed = pos_slice.apply(self["embed"], -2)[None] 

1545 if ln_folded: 

1546 embed = _ln_then_project(embed) 

1547 elif project_2d is not None: 

1548 embed = embed @ project_2d 

1549 labels.append("embed") 

1550 components.append(embed) 

1551 if self.has_pos_embed: 1551 ↛ 1560line 1551 didn't jump to line 1560 because the condition on line 1551 was always true

1552 pos_embed = pos_slice.apply(self["pos_embed"], -2)[None] 

1553 if ln_folded: 

1554 pos_embed = _ln_then_project(pos_embed) 

1555 elif project_2d is not None: 

1556 pos_embed = pos_embed @ project_2d 

1557 labels.append("pos_embed") 

1558 components.append(pos_embed) 

1559 # If we didn't expand the neurons, the MLP biases are already included in the MLP outputs. 

1560 bias_full = self.model.accumulated_bias(layer, mlp_input, include_mlp_biases=expand_neurons) 

1561 if ln_folded: 

1562 # Expand bias to per-position d_model shape so LN can center, then project. 

1563 expand_shape: tuple = (1,) + tuple(head_stack.shape[1:-1]) + (self.model.cfg.d_model,) 

1564 bias = _ln_then_project(bias_full.expand(expand_shape)) 

1565 else: 

1566 if project_2d is not None: 

1567 # Bias is [d_model], so project post-hoc for shape compatibility — no memory win here. 

1568 bias_full = bias_full @ project_2d 

1569 bias = bias_full.expand((1,) + head_stack.shape[1:]) 

1570 labels.append("bias") 

1571 components.append(bias) 

1572 residual_stack = torch.cat(components, dim=0) 

1573 if apply_ln and not ln_folded: 

1574 residual_stack = self.apply_ln_to_stack( 

1575 residual_stack, layer, pos_slice=pos_slice, mlp_input=mlp_input 

1576 ) 

1577 

1578 if squeeze_projected: 

1579 residual_stack = residual_stack.squeeze(-1) 

1580 

1581 if return_labels: 

1582 return residual_stack, labels 

1583 else: 

1584 return residual_stack