Coverage for transformer_lens/ActivationCache.py: 94%

466 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-01 16:23 +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.HookedTransformer import HookedTransformer 

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.HookedTransformer.HookedTransformer.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 import HookedTransformer 

76 >>> model = HookedTransformer.from_pretrained("tiny-stories-1M") 

77 Loaded pretrained model tiny-stories-1M into HookedTransformer 

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.HookedTransformer`, and will not work with other models. It's also 

106 designed to be used with all activations of :class:`transformer_lens.HookedTransformer` being 

107 cached, and some internal methods will break without that. 

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 # Helper methods require HT-internal structure; bridge users only use cache_dict. 

149 self.model = cast("HookedTransformer", model) 

150 self.has_batch_dim = has_batch_dim 

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

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

153 

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

155 

156 def _batch_size(self) -> int: 

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

158 

159 Caches may hold non-batch entries alongside genuinely batched 

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

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

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

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

164 reliable signal where max/min are not. 

165 """ 

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

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

168 

169 def remove_batch_dim(self) -> ActivationCache: 

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

171 

172 Returns: 

173 The ActivationCache with the batch dimension removed. 

174 """ 

175 if self.has_batch_dim: 

176 batch_size = self._batch_size() 

177 assert ( 

178 batch_size == 1 

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

180 for key in self.cache_dict: 

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

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

183 self.has_batch_dim = False 

184 else: 

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

186 return self 

187 

188 def __repr__(self) -> str: 

189 """Representation of the ActivationCache. 

190 

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

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

193 describes the object. 

194 """ 

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

196 

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

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

199 

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

201 shorthand naming conventions. 

202 

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

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

205 

206 

207 Args: 

208 key: 

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

210 

211 Returns: 

212 The cached activation tensor corresponding to the given key. 

213 """ 

214 if key in self.cache_dict: 

215 return self.cache_dict[key] 

216 elif type(key) == str: 

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

218 else: 

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

220 if key[1] < 0: 

221 # Supports negative indexing on the layer dimension 

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

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

224 

225 def __len__(self) -> int: 

226 """Length of the ActivationCache. 

227 

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

229 activations in the cache). 

230 """ 

231 return len(self.cache_dict) 

232 

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

234 """Move the Cache to a Device. 

235 

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

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

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

239 `compute_head_results`. 

240 

241 Args: 

242 device: 

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

244 

245 """ 

246 warn_if_mps(device) 

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

248 return self 

249 

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

251 """Toggle Autodiff Globally. 

252 

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

254 

255 Warning: 

256 

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

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

259 realise what you're doing. 

260 

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

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

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

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

265 than its worth. 

266 

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

268 or decorator achieves similar effects: 

269 

270 >>> with torch.inference_mode(): 

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

272 >>> y.requires_grad 

273 False 

274 """ 

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

276 torch.set_grad_enabled(mode) 

277 

278 def keys(self): 

279 """Keys of the ActivationCache. 

280 

281 Examples: 

282 

283 >>> from transformer_lens import HookedTransformer 

284 >>> model = HookedTransformer.from_pretrained("tiny-stories-1M") 

285 Loaded pretrained model tiny-stories-1M into HookedTransformer 

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

287 >>> list(cache.keys())[0:3] 

288 ['hook_embed', 'hook_pos_embed', 'blocks.0.hook_resid_pre'] 

289 

290 Returns: 

291 List of all keys. 

292 """ 

293 return self.cache_dict.keys() 

294 

295 def values(self): 

296 """Values of the ActivationCache. 

297 

298 Returns: 

299 List of all values. 

300 """ 

301 return self.cache_dict.values() 

302 

303 def items(self): 

304 """Items of the ActivationCache. 

305 

306 Returns: 

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

308 """ 

309 return self.cache_dict.items() 

310 

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

312 """ActivationCache Iterator. 

313 

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

315 cache. 

316 

317 Examples: 

318 

319 >>> from transformer_lens import HookedTransformer 

320 >>> model = HookedTransformer.from_pretrained("tiny-stories-1M") 

321 Loaded pretrained model tiny-stories-1M into HookedTransformer 

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

323 >>> cache_interesting_names = [] 

324 >>> for key in cache: 

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

326 ... cache_interesting_names.append(key) 

327 >>> print(cache_interesting_names[0:3]) 

328 ['hook_embed', 'hook_pos_embed', 'blocks.0.hook_resid_pre'] 

329 

330 Returns: 

331 Iterator over the cache. 

332 """ 

333 return self.cache_dict.__iter__() 

334 

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

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

337 

338 Args: 

339 batch_slice: 

340 The slice to apply to the batch dimension. 

341 

342 Returns: 

343 The ActivationCache with the batch dimension sliced. 

344 """ 

345 if not isinstance(batch_slice, Slice): 

346 batch_slice = Slice(batch_slice) 

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

348 assert ( 

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

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

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

352 batch_size = self._batch_size() 

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

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

355 new_cache_dict = { 

356 name: ( 

357 batch_slice.apply(param, dim=0) 

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

359 else param 

360 ) 

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

362 } 

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

364 

365 def accumulated_resid( 

366 self, 

367 layer: Optional[int] = None, 

368 incl_mid: bool = False, 

369 apply_ln: bool = False, 

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

371 mlp_input: bool = False, 

372 return_labels: bool = False, 

373 ) -> Union[ 

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

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

376 ]: 

377 """Accumulated Residual Stream. 

378 

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

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

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

382 residual stream. 

383 

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

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

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

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

388 

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

390 

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

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

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

394 handled consistently. 

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

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

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

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

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

400 typically not desired when excluding bias terms. 

401 

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

403 

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

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

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

407 MLP neuron. 

408 

409 Examples: 

410 

411 Logit Lens analysis can be done as follows: 

412 

413 >>> from transformer_lens import HookedTransformer 

414 >>> import torch 

415 >>> import pandas as pd 

416 

417 >>> model = HookedTransformer.from_pretrained("tiny-stories-1M", device="cpu", fold_ln=True) 

418 Loaded pretrained model tiny-stories-1M into HookedTransformer 

419 

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

421 >>> answer = " road" 

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

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

424 >>> print(answer_token) 

425 2975 

426 

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

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

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

430 torch.Size([9, 64]) 

431 

432 

433 >>> W_U = model.W_U 

434 >>> print(W_U.shape) 

435 torch.Size([64, 50257]) 

436 

437 >>> # Project to vocabulary without unembedding bias 

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

439 >>> print(layers_logits.shape) 

440 torch.Size([9, 50257]) 

441 

442 >>> # If you want to apply the unembedding bias, add b_U when present: 

443 >>> # b_U = getattr(model, "b_U", None) 

444 >>> # layers_logits = layers_logits + b_U if b_U is not None else layers_logits 

445 >>> # print(layers_logits.shape) 

446 torch.Size([9, 50257]) 

447 

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

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

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

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

452 0_pre 4442 

453 1_pre 382 

454 2_pre 982 

455 3_pre 1160 

456 4_pre 408 

457 5_pre 145 

458 6_pre 78 

459 7_pre 387 

460 final_post 6 

461 dtype: int64 

462 

463 Args: 

464 layer: 

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

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

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

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

469 streams up to the input to layer l. 

470 incl_mid: 

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

472 apply_ln: 

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

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

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

476 activations into the format expected by the unembedding layer. 

477 pos_slice: 

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

479 mlp_input: 

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

481 input rather than the attention input. 

482 return_labels: 

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

484 labelling graphs. 

485 

486 Returns: 

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

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

489 """ 

490 if not isinstance(pos_slice, Slice): 

491 pos_slice = Slice(pos_slice) 

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

493 # Default to the residual stream immediately pre unembed 

494 layer = self.model.cfg.n_layers 

495 assert isinstance(layer, int) 

496 labels = [] 

497 components_list = [] 

498 for l in range(layer + 1): 

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

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

501 labels.append("final_post") 

502 continue 

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

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

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

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

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

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

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

510 if apply_ln: 

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

512 components = self.apply_ln_to_stack( 

513 components, 

514 layer, 

515 pos_slice=pos_slice, 

516 mlp_input=mlp_input, 

517 has_batch_dim=self.has_batch_dim, 

518 recompute_ln=recompute_ln, 

519 ) 

520 if return_labels: 

521 return components, labels 

522 else: 

523 return components 

524 

525 def logit_attrs( 

526 self, 

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

528 tokens: Union[ 

529 str, 

530 int, 

531 Int[torch.Tensor, ""], 

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

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

534 ], 

535 incorrect_tokens: Optional[ 

536 Union[ 

537 str, 

538 int, 

539 Int[torch.Tensor, ""], 

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

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

542 ] 

543 ] = None, 

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

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

546 has_batch_dim: bool = True, 

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

548 """Logit Attributions. 

549 

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

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

552 

553 It does this by: 

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

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

556 directions. 

557 

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

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

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

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

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

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

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

565 

566 Warning: 

567 

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

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

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

571 

572 Args: 

573 residual_stack: 

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

575 tokens: 

576 Tokens to compute logit attributions on. 

577 incorrect_tokens: 

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

579 incorrect_tokens. Must have the same shape as tokens. 

580 pos_slice: 

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

582 batch_slice: 

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

584 None, do nothing. 

585 has_batch_dim: 

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

587 

588 Returns: 

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

590 was provided. 

591 """ 

592 if not isinstance(pos_slice, Slice): 

593 pos_slice = Slice(pos_slice) 

594 

595 if not isinstance(batch_slice, Slice): 

596 batch_slice = Slice(batch_slice) 

597 

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

599 tokens_for_shape_check = tokens 

600 

601 if isinstance(tokens_for_shape_check, str): 

602 tokens_for_shape_check = torch.as_tensor( 

603 self.model.to_single_token(tokens_for_shape_check) 

604 ) 

605 elif isinstance(tokens_for_shape_check, int): 

606 tokens_for_shape_check = torch.as_tensor(tokens_for_shape_check) 

607 

608 logit_directions = self.model.tokens_to_residual_directions(tokens) 

609 

610 if incorrect_tokens is not None: 

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

612 incorrect_tokens_for_shape_check = incorrect_tokens 

613 

614 if isinstance(incorrect_tokens_for_shape_check, str): 

615 incorrect_tokens_for_shape_check = torch.as_tensor( 

616 self.model.to_single_token(incorrect_tokens_for_shape_check) 

617 ) 

618 elif isinstance(incorrect_tokens_for_shape_check, int): 

619 incorrect_tokens_for_shape_check = torch.as_tensor(incorrect_tokens_for_shape_check) 

620 

621 if tokens_for_shape_check.shape != incorrect_tokens_for_shape_check.shape: 

622 raise ValueError( 

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

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

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

626 ) 

627 

628 # If incorrect_tokens was provided, take the logit difference 

629 logit_directions = logit_directions - self.model.tokens_to_residual_directions( 

630 incorrect_tokens 

631 ) 

632 

633 scaled_residual_stack = self.apply_ln_to_stack( 

634 residual_stack, 

635 layer=-1, 

636 pos_slice=pos_slice, 

637 batch_slice=batch_slice, 

638 has_batch_dim=has_batch_dim, 

639 ) 

640 

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

642 return logit_attrs 

643 

644 def decompose_resid( 

645 self, 

646 layer: Optional[int] = None, 

647 mlp_input: bool = False, 

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

649 apply_ln: bool = False, 

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

651 incl_embeds: bool = True, 

652 return_labels: bool = False, 

653 ) -> Union[ 

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

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

656 ]: 

657 """Decompose the Residual Stream. 

658 

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

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

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

662 

663 Args: 

664 layer: 

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

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

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

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

669 accumulated streams up to the input to layer l 

670 mlp_input: 

671 Whether to include attn_out for the current 

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

673 rather than the Attn input. 

674 mode: 

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

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

677 attention components. Defaults to "all". 

678 apply_ln: 

679 Whether to apply LayerNorm to the stack. 

680 pos_slice: 

681 A slice object to apply to the pos dimension. 

682 Defaults to None, do nothing. 

683 incl_embeds: 

684 Whether to include embed & pos_embed 

685 return_labels: 

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

687 Useful for labelling graphs. 

688 

689 Returns: 

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

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

692 """ 

693 if not isinstance(pos_slice, Slice): 

694 pos_slice = Slice(pos_slice) 

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

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

697 # Default to the residual stream immediately pre unembed 

698 layer = self.model.cfg.n_layers 

699 assert isinstance(layer, int) 

700 

701 incl_attn = mode != "mlp" 

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

703 components_list = [] 

704 labels = [] 

705 if incl_embeds: 

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

707 components_list = [self["hook_embed"]] 

708 labels.append("embed") 

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

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

711 labels.append("pos_embed") 

712 

713 for l in range(layer): 

714 if incl_attn: 

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

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

717 if incl_mlp: 

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

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

720 if mlp_input and incl_attn: 

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

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

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

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

725 if apply_ln: 

726 components = self.apply_ln_to_stack( 

727 components, layer, pos_slice=pos_slice, mlp_input=mlp_input 

728 ) 

729 if return_labels: 

730 return components, labels 

731 else: 

732 return components 

733 

734 def compute_head_results( 

735 self, 

736 ): 

737 """Compute Head Results. 

738 

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

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

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

742 be useful if you forget. 

743 

744 Works for both HookedTransformer and TransformerBridge — bridge exposes 

745 ``blocks[i].attn.W_O`` via its component-mapping compatibility shim. 

746 """ 

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

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

749 if first_key in self.cache_dict: 

750 val = self.cache_dict[first_key] 

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

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

753 return 

754 # Remove stale 3D entries before recomputing 

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

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

757 if key in self.cache_dict: 

758 del self.cache_dict[key] 

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

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

761 # cache_dict directly. 

762 

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

764 z = einops.rearrange( 

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

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

767 ) 

768 

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

770 block = self.model.blocks[layer] 

771 result = z * block.attn.W_O 

772 

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

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

775 

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

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

778 

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

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

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

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

783 ``cfg.layers_block_type``. 

784 

785 Args: 

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

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

788 

789 Returns: 

790 Ascending list of SSM block indices. 

791 """ 

792 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

793 find_ssm_mixer, 

794 ) 

795 

796 bridge = self.model 

797 layers: List[int] = [] 

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

799 mixer = find_ssm_mixer(block) 

800 if mixer is None: 

801 continue 

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

803 continue 

804 layers.append(i) 

805 return layers 

806 

807 def _over_ssm_layers( 

808 self, 

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

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

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

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

813 

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

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

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

817 dict over the SSM layers (heterogeneous hybrids). 

818 """ 

819 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

820 find_ssm_mixer, 

821 ) 

822 

823 bridge = self.model 

824 indices = self.ssm_layers(mixer_type=mixer_type) 

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

826 raise RuntimeError( 

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

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

829 ) 

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

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

832 } 

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

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

835 return results 

836 

837 def compute_ssm_effective_attention( 

838 self, 

839 layer: Optional[int] = None, 

840 include_dt_scaling: bool = False, 

841 per_state_coord: bool = False, 

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

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

844 

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

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

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

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

849 

850 Args: 

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

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

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

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

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

856 

857 Returns: 

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

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

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

861 

862 Raises: 

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

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

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

866 """ 

867 import inspect 

868 

869 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

870 find_ssm_mixer, 

871 ) 

872 

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

874 fn = cast(Any, mixer).compute_effective_attention 

875 params = inspect.signature(fn).parameters 

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

877 for name, value in ( 

878 ("include_dt_scaling", include_dt_scaling), 

879 ("per_state_coord", per_state_coord), 

880 ): 

881 if name in params: 

882 kwargs[name] = value 

883 elif value: 

884 raise ValueError( 

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

886 f"support {name}=True." 

887 ) 

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

889 return result 

890 

891 if layer is not None: 

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

893 if single is None: 

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

895 return _call(single, layer) 

896 return self._over_ssm_layers(_call) 

897 

898 def compute_ssm_state( 

899 self, 

900 layer: Optional[int] = None, 

901 time_step: Optional[int] = None, 

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

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

904 

905 The single discovery surface for recurrent state across families — 

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

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

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

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

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

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

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

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

914 shares one mixer type. 

915 

916 Args: 

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

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

919 

920 Returns: 

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

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

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

924 

925 Raises: 

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

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

928 """ 

929 from transformer_lens.model_bridge.generalized_components import ( 

930 GatedDeltaNetBridge, 

931 SSM2MixerBridge, 

932 SSMMixerBridge, 

933 ) 

934 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

935 find_ssm_mixer, 

936 ) 

937 

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

939 state_mixers = (SSMMixerBridge, SSM2MixerBridge, GatedDeltaNetBridge) 

940 

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

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

943 self, layer_idx=layer_idx, time_step=time_step 

944 ) 

945 return state 

946 

947 if layer is not None: 

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

949 if not isinstance(single, state_mixers): 

950 raise TypeError( 

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

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

953 ) 

954 return _call(single, layer) 

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

956 

957 def stack_head_results( 

958 self, 

959 layer: int = -1, 

960 return_labels: bool = False, 

961 incl_remainder: bool = False, 

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

963 apply_ln: bool = False, 

964 ) -> Union[ 

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

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

967 ]: 

968 """Stack Head Results. 

969 

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

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

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

973 notation). 

974 

975 Args: 

976 layer: 

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

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

979 return_labels: 

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

981 incl_remainder: 

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

983 pos_slice: 

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

985 apply_ln: 

986 Whether to apply LayerNorm to the stack. 

987 """ 

988 if not isinstance(pos_slice, Slice): 

989 pos_slice = Slice(pos_slice) 

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

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

992 # Default to the residual stream immediately pre unembed 

993 layer = self.model.cfg.n_layers 

994 

995 # Idempotent; cleans up stale Bridge entries 

996 self.compute_head_results() 

997 

998 components: Any = [] 

999 labels = [] 

1000 for l in range(layer): 

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

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

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

1004 if components: 

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

1006 components = einops.rearrange( 

1007 components, 

1008 "... concat_head_index d_model -> concat_head_index ... d_model", 

1009 ) 

1010 if incl_remainder: 

1011 remainder = pos_slice.apply( 

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

1013 ) - components.sum(dim=0) 

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

1015 labels.append("remainder") 

1016 elif incl_remainder: 

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

1018 components = torch.cat( 

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

1020 ) 

1021 labels.append("remainder") 

1022 else: 

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

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

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

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

1027 components = torch.zeros( 

1028 0, 

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

1030 device=self.model.cfg.device, 

1031 ) 

1032 

1033 if apply_ln: 

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

1035 

1036 if return_labels: 

1037 return components, labels 

1038 else: 

1039 return components 

1040 

1041 def stack_activation( 

1042 self, 

1043 activation_name: str, 

1044 layer: int = -1, 

1045 sublayer_type: Optional[str] = None, 

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

1047 """Stack Activations. 

1048 

1049 Flexible way to stack activations with a given name. 

1050 

1051 Args: 

1052 activation_name: 

1053 The name of the activation to be stacked 

1054 layer: 

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

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

1057 sublayer_type: 

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

1059 inferred. 

1060 incl_remainder: 

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

1062 """ 

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

1064 # Default to the residual stream immediately pre unembed 

1065 layer = self.model.cfg.n_layers 

1066 

1067 components = [] 

1068 for l in range(layer): 

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

1070 

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

1072 

1073 def get_neuron_results( 

1074 self, 

1075 layer: int, 

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

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

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

1079 ) -> torch.Tensor: 

1080 """Get Neuron Results. 

1081 

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

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

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

1085 

1086 Args: 

1087 layer: 

1088 Layer index. 

1089 neuron_slice: 

1090 Slice of the neuron. 

1091 pos_slice: 

1092 Slice of the positions. 

1093 project_output_onto: 

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

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

1096 intermediate is never materialized. 

1097 

1098 Returns: 

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

1100 (1D projection). 

1101 """ 

1102 if not isinstance(neuron_slice, Slice): 

1103 neuron_slice = Slice(neuron_slice) 

1104 if not isinstance(pos_slice, Slice): 

1105 pos_slice = Slice(pos_slice) 

1106 

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

1108 block = self.model.blocks[layer] 

1109 W_out = block.mlp.W_out 

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

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

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

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

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

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

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

1117 if project_output_onto is None: 

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

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

1120 projected = W_out @ project_output_onto 

1121 if projected.ndim == 1: 

1122 return neuron_acts * projected 

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

1124 

1125 def _get_cached_ln_scale( 

1126 self, 

1127 layer: Optional[int], 

1128 mlp_input: bool, 

1129 pos_slice: Slice, 

1130 batch_slice: Optional[Slice] = None, 

1131 ) -> torch.Tensor: 

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

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

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

1135 """ 

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

1137 key = "ln_final.hook_scale" 

1138 else: 

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

1140 try: 

1141 scale = self[key] 

1142 except KeyError as e: 

1143 raise KeyError( 

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

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

1146 f"under different module paths)." 

1147 ) from e 

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

1149 if batch_slice is not None and self.has_batch_dim: 

1150 scale = batch_slice.apply(scale) 

1151 return scale 

1152 

1153 def _stack_neuron_results_apply_ln_projected( 

1154 self, 

1155 layer: int, 

1156 pos_slice: Slice, 

1157 neuron_slice: Slice, 

1158 project_2d: torch.Tensor, 

1159 ) -> torch.Tensor: 

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

1161 

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

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

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

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

1166 

1167 """ 

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

1169 

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

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

1172 

1173 components: list = [] 

1174 for l in range(layer): 

1175 block = self.model.blocks[l] 

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

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

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

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

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

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

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

1183 else: 

1184 lin_form_l = W_proj_l 

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

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

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

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

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

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

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

1192 return torch.zeros( 

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

1194 ) 

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

1196 return einops.rearrange( 

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

1198 ) 

1199 

1200 def stack_neuron_results( 

1201 self, 

1202 layer: int, 

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

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

1205 return_labels: bool = False, 

1206 incl_remainder: bool = False, 

1207 apply_ln: bool = False, 

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

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

1210 """Stack Neuron Results 

1211 

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

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

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

1215 into attribution by specific neurons. 

1216 

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

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

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

1220 

1221 Args: 

1222 layer: 

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

1224 in [1, n_layers] 

1225 pos_slice: 

1226 Slice of the positions. 

1227 neuron_slice: 

1228 Slice of the neurons. 

1229 return_labels: 

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

1231 incl_remainder: 

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

1233 apply_ln: 

1234 Whether to apply LayerNorm to the stack. 

1235 project_output_onto: 

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

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

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

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

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

1241 """ 

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

1243 # Default to the residual stream immediately pre unembed 

1244 layer = self.model.cfg.n_layers 

1245 

1246 if not isinstance(neuron_slice, Slice): 

1247 neuron_slice = Slice(neuron_slice) 

1248 if not isinstance(pos_slice, Slice): 

1249 pos_slice = Slice(pos_slice) 

1250 

1251 project_2d, squeeze_projected = _normalize_projection_to_2d(project_output_onto) 

1252 

1253 d_mlp = self.model.cfg.d_mlp 

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

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

1256 torch.arange(d_mlp), dim=0 

1257 ) 

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

1259 neuron_labels = np.array([neuron_labels]) 

1260 

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

1262 components: Any 

1263 ln_folded = apply_ln and project_2d is not None 

1264 if ln_folded: 

1265 assert project_2d is not None # narrow for mypy 

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

1267 components = self._stack_neuron_results_apply_ln_projected( 

1268 layer, pos_slice, neuron_slice, project_2d 

1269 ) 

1270 if incl_remainder: 

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

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

1273 resid_post_ln = self.apply_ln_to_stack( 

1274 resid_post[None], layer, pos_slice=pos_slice 

1275 )[0] 

1276 remainder = resid_post_ln @ project_2d 

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

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

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

1280 labels.append("remainder") 

1281 else: 

1282 per_layer: list = [] 

1283 for l in range(layer): 

1284 per_layer.append( 

1285 self.get_neuron_results( 

1286 l, 

1287 pos_slice=pos_slice, 

1288 neuron_slice=neuron_slice, 

1289 project_output_onto=project_2d, 

1290 ) 

1291 ) 

1292 if per_layer: 

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

1294 components = einops.rearrange( 

1295 components, 

1296 "... concat_neuron_index d_model -> concat_neuron_index ... d_model", 

1297 ) 

1298 if incl_remainder: 

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

1300 if project_2d is not None: 

1301 remainder_full = remainder_full @ project_2d 

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

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

1304 labels.append("remainder") 

1305 elif incl_remainder: 

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

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

1308 remainder_full = remainder_full @ project_2d 

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

1310 labels.append("remainder") 

1311 else: 

1312 empty_shape_src = pos_slice.apply(self["hook_embed"], 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 empty_shape_src = empty_shape_src @ project_2d 

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

1316 

1317 if apply_ln: 

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

1319 

1320 if squeeze_projected: 

1321 components = components.squeeze(-1) 

1322 

1323 if return_labels: 

1324 return components, labels 

1325 else: 

1326 return components 

1327 

1328 def apply_ln_to_stack( 

1329 self, 

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

1331 layer: Optional[int] = None, 

1332 mlp_input: bool = False, 

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

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

1335 has_batch_dim: bool = True, 

1336 recompute_ln: bool = False, 

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

1338 """Apply Layer Norm to a Stack. 

1339 

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

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

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

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

1344 

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

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

1347 applying a new LayerNorm. 

1348 

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

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

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

1352 

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

1354 

1355 Args: 

1356 residual_stack: 

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

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

1359 or position dimensions. 

1360 layer: 

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

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

1363 mlp_input: 

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

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

1366 pos_slice: 

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

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

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

1370 Defaults to None, do nothing. 

1371 batch_slice: 

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

1373 has_batch_dim: 

1374 Whether residual_stack has a batch dimension. 

1375 recompute_ln: 

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

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

1378 

1379 """ 

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

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

1382 return residual_stack 

1383 if not isinstance(pos_slice, Slice): 

1384 pos_slice = Slice(pos_slice) 

1385 if not isinstance(batch_slice, Slice): 

1386 batch_slice = Slice(batch_slice) 

1387 

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

1389 # Default to the residual stream immediately pre unembed 

1390 layer = self.model.cfg.n_layers 

1391 

1392 if has_batch_dim: 

1393 # Apply batch slice to the stack 

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

1395 

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

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

1398 ln_final = self.model.ln_final 

1399 results = [] 

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

1401 x = residual_stack[i] 

1402 original_shape = x.shape 

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

1404 if not has_batch_dim: 

1405 x = x.unsqueeze(0) 

1406 if x.ndim == 2: 

1407 x = x.unsqueeze(1) 

1408 out = ln_final(x) 

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

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

1411 

1412 # Center the stack onlny if the model uses LayerNorm 

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

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

1415 

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

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

1418 

1419 return residual_stack / scale 

1420 

1421 def get_full_resid_decomposition( 

1422 self, 

1423 layer: Optional[int] = None, 

1424 mlp_input: bool = False, 

1425 expand_neurons: bool = True, 

1426 apply_ln: bool = False, 

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

1428 return_labels: bool = False, 

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

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

1431 """Get the full Residual Decomposition. 

1432 

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

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

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

1436 

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

1438 

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

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

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

1442 else ``L`` rows) 

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

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

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

1446 

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

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

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

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

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

1452 

1453 Args: 

1454 layer: 

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

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

1457 just embed and pos_embed 

1458 mlp_input: 

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

1460 layer, since that's the unembed. 

1461 expand_neurons: 

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

1463 MLP layer outputs. 

1464 apply_ln: 

1465 Whether to apply LayerNorm to the stack. 

1466 pos_slice: 

1467 Slice of the positions to take. 

1468 return_labels: 

1469 Whether to return the labels. 

1470 project_output_onto: 

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

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

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

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

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

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

1477 """ 

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

1479 # Default to the residual stream immediately pre unembed 

1480 layer = self.model.cfg.n_layers 

1481 assert layer is not None # keep mypy happy 

1482 

1483 if not isinstance(pos_slice, Slice): 

1484 pos_slice = Slice(pos_slice) 

1485 

1486 project_2d, squeeze_projected = _normalize_projection_to_2d(project_output_onto) 

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

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

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

1490 ln_folded = apply_ln and project_2d is not None 

1491 

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

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

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

1495 

1496 head_stack, head_labels = self.stack_head_results( 

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

1498 ) 

1499 if ln_folded: 

1500 head_stack = _ln_then_project(head_stack) 

1501 elif project_2d is not None: 

1502 head_stack = head_stack @ project_2d 

1503 labels = head_labels 

1504 components = [head_stack] 

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

1506 if expand_neurons: 

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

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

1509 neuron_stack, neuron_labels = self.stack_neuron_results( 

1510 layer, 

1511 pos_slice=pos_slice, 

1512 return_labels=True, 

1513 apply_ln=ln_folded, 

1514 project_output_onto=project_2d, 

1515 ) 

1516 labels.extend(neuron_labels) 

1517 components.append(neuron_stack) 

1518 else: 

1519 # Get the stack of just the MLP outputs 

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

1521 # just for MLP outputs 

1522 mlp_stack, mlp_labels = self.decompose_resid( 

1523 layer, 

1524 mlp_input=mlp_input, 

1525 pos_slice=pos_slice, 

1526 incl_embeds=False, 

1527 mode="mlp", 

1528 return_labels=True, 

1529 ) 

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

1531 mlp_stack = _ln_then_project(mlp_stack) 

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

1533 mlp_stack = mlp_stack @ project_2d 

1534 labels.extend(mlp_labels) 

1535 components.append(mlp_stack) 

1536 

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

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

1539 if ln_folded: 

1540 embed = _ln_then_project(embed) 

1541 elif project_2d is not None: 

1542 embed = embed @ project_2d 

1543 labels.append("embed") 

1544 components.append(embed) 

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

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

1547 if ln_folded: 

1548 pos_embed = _ln_then_project(pos_embed) 

1549 elif project_2d is not None: 

1550 pos_embed = pos_embed @ project_2d 

1551 labels.append("pos_embed") 

1552 components.append(pos_embed) 

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

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

1555 if ln_folded: 

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

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

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

1559 else: 

1560 if project_2d is not None: 

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

1562 bias_full = bias_full @ project_2d 

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

1564 labels.append("bias") 

1565 components.append(bias) 

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

1567 if apply_ln and not ln_folded: 

1568 residual_stack = self.apply_ln_to_stack( 

1569 residual_stack, layer, pos_slice=pos_slice, mlp_input=mlp_input 

1570 ) 

1571 

1572 if squeeze_projected: 

1573 residual_stack = residual_stack.squeeze(-1) 

1574 

1575 if return_labels: 

1576 return residual_stack, labels 

1577 else: 

1578 return residual_stack