Coverage for transformer_lens/ActivationCache.py: 98%

498 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-10-23 20:45 +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 torch 

33from jaxtyping import Float, Int 

34from typing_extensions import Literal 

35 

36import transformer_lens.utilities as utils 

37from transformer_lens.utilities import Slice, SliceInput, warn_if_mps 

38 

39if TYPE_CHECKING: 

40 from transformer_lens.model_protocol import TransformerLensModelWithWeights 

41 

42 

43def _normalize_projection_to_2d( 

44 project: Optional[torch.Tensor], 

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

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

47 if project is None: 

48 return None, False 

49 if project.ndim == 1: 

50 return project.unsqueeze(-1), True 

51 return project, False 

52 

53 

54class ActivationCache: 

55 """Activation Cache. 

56 

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

58 variety of helper functions to investigate them. 

59 

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

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

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

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

64 

65 Examples: 

66 

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

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

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

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

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

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

73 

74 >>> from transformer_lens.model_bridge import TransformerBridge 

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

76 >>> model.enable_compatibility_mode() 

77 

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

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

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

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

82 

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

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

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

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

87 

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

89 >>> print(labels[most_important_component_idx]) 

90 3_attn_out 

91 

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

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

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

95 

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

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

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

99 :meth:`ActivationCache.accumulated_resid`. 

100 

101 Warning: 

102 

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

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

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

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

107 

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

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

110 

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

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

113 [batch, head_index, query_pos, key_pos]. 

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

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

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

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

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

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

120 

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

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

123 

124 Type annotations are in the following form: 

125 

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

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

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

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

130 

131 Args: 

132 cache_dict: 

133 A dictionary of cached activations from a model run. 

134 model: 

135 The model that the activations are from. 

136 has_batch_dim: 

137 Whether the activations have a batch dimension. 

138 """ 

139 

140 def __init__( 

141 self, 

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

143 model: Any, 

144 has_batch_dim: bool = True, 

145 ): 

146 self.cache_dict = cache_dict 

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

148 # weight-processing surface, which TransformerBridge exposes. 

149 self.model = cast("TransformerLensModelWithWeights", 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, batch_size: Optional[int] = None) -> ActivationCache: 

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

171 

172 Args: 

173 batch_size: 

174 The true batch size, when the caller knows it. By default it is inferred from 

175 the most common leading dimension of the cached entries, which is wrong for a 

176 cache holding only flattened or position-indexed activations. 

177 

178 Returns: 

179 The ActivationCache with the batch dimension removed. 

180 """ 

181 if self.has_batch_dim: 

182 if batch_size is None: 

183 batch_size = self._batch_size() 

184 assert ( 

185 batch_size == 1 

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

187 for key in self.cache_dict: 

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

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

190 self.has_batch_dim = False 

191 else: 

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

193 return self 

194 

195 def __repr__(self) -> str: 

196 """Representation of the ActivationCache. 

197 

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

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

200 describes the object. 

201 """ 

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

203 

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

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

206 

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

208 shorthand naming conventions. 

209 

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

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

212 

213 

214 Args: 

215 key: 

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

217 

218 Returns: 

219 The cached activation tensor corresponding to the given key. 

220 """ 

221 if key in self.cache_dict: 

222 return self.cache_dict[key] 

223 elif type(key) == str: 

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

225 else: 

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

227 if key[1] < 0: 

228 # Supports negative indexing on the layer dimension 

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

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

231 

232 def __len__(self) -> int: 

233 """Length of the ActivationCache. 

234 

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

236 activations in the cache). 

237 """ 

238 return len(self.cache_dict) 

239 

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

241 """Move the Cache to a Device. 

242 

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

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

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

246 `compute_head_results`. 

247 

248 Args: 

249 device: 

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

251 

252 """ 

253 warn_if_mps(device) 

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

255 return self 

256 

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

258 """Toggle Autodiff Globally. 

259 

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

261 

262 Warning: 

263 

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

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

266 realise what you're doing. 

267 

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

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

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

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

272 than its worth. 

273 

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

275 or decorator achieves similar effects: 

276 

277 >>> with torch.inference_mode(): 

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

279 >>> y.requires_grad 

280 False 

281 """ 

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

283 torch.set_grad_enabled(mode) 

284 

285 def keys(self): 

286 """Keys of the ActivationCache. 

287 

288 Examples: 

289 

290 >>> from transformer_lens.model_bridge import TransformerBridge 

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

292 >>> model.enable_compatibility_mode() 

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

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

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

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

297 'blocks.0.hook_resid_pre'] 

298 

299 Returns: 

300 List of all keys. 

301 """ 

302 return self.cache_dict.keys() 

303 

304 def values(self): 

305 """Values of the ActivationCache. 

306 

307 Returns: 

308 List of all values. 

309 """ 

310 return self.cache_dict.values() 

311 

312 def items(self): 

313 """Items of the ActivationCache. 

314 

315 Returns: 

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

317 """ 

318 return self.cache_dict.items() 

319 

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

321 """ActivationCache Iterator. 

322 

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

324 cache. 

325 

326 Examples: 

327 

328 >>> from transformer_lens.model_bridge import TransformerBridge 

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

330 >>> model.enable_compatibility_mode() 

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

332 >>> cache_interesting_names = [] 

333 >>> for key in cache: 

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

335 ... cache_interesting_names.append(key) 

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

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

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

339 'blocks.0.hook_resid_pre'] 

340 

341 Returns: 

342 Iterator over the cache. 

343 """ 

344 return self.cache_dict.__iter__() 

345 

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

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

348 

349 Args: 

350 batch_slice: 

351 The slice to apply to the batch dimension. 

352 

353 Returns: 

354 The ActivationCache with the batch dimension sliced. 

355 """ 

356 if not isinstance(batch_slice, Slice): 

357 batch_slice = Slice(batch_slice) 

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

359 assert ( 

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

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

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

363 batch_size = self._batch_size() 

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

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

366 new_cache_dict = { 

367 name: ( 

368 batch_slice.apply(param, dim=0) 

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

370 else param 

371 ) 

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

373 } 

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

375 

376 def accumulated_resid( 

377 self, 

378 layer: Optional[int] = None, 

379 incl_mid: bool = False, 

380 apply_ln: bool = False, 

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

382 mlp_input: bool = False, 

383 return_labels: bool = False, 

384 ) -> Union[ 

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

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

387 ]: 

388 """Accumulated Residual Stream. 

389 

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

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

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

393 residual stream. 

394 

395 :meth:`logit_lens` does the whole projection below in one call (final norm, ``W_U``, 

396 ``b_U`` and the adapter's output transform, chunked over the vocabulary); the recipe 

397 here shows what it computes. 

398 

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

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

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

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

403 

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

405 

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

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

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

409 handled consistently. 

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

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

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

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

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

415 typically not desired when excluding bias terms. 

416 

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

418 

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

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

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

422 MLP neuron. 

423 

424 Examples: 

425 

426 Logit Lens analysis can be done as follows: 

427 

428 >>> from transformer_lens.model_bridge import TransformerBridge 

429 >>> import torch 

430 >>> import pandas as pd 

431 

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

433 >>> model.enable_compatibility_mode() 

434 

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

436 >>> answer = " road" 

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

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

439 >>> print(answer_token) 

440 2975 

441 

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

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

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

445 torch.Size([9, 64]) 

446 

447 

448 >>> W_U = model.W_U 

449 >>> print(W_U.shape) 

450 torch.Size([64, 50257]) 

451 

452 >>> # Project to vocabulary without unembedding bias 

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

454 >>> print(layers_logits.shape) 

455 torch.Size([9, 50257]) 

456 

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

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

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

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

461 >>> print(with_bias.shape) 

462 torch.Size([9, 50257]) 

463 

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

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

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

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

468 0_pre 4442 

469 1_pre 382 

470 2_pre 982 

471 3_pre 1160 

472 4_pre 408 

473 5_pre 145 

474 6_pre 78 

475 7_pre 387 

476 final_post 6 

477 dtype: int64 

478 

479 Args: 

480 layer: 

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

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

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

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

485 streams up to the input to layer l. 

486 incl_mid: 

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

488 apply_ln: 

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

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

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

492 activations into the format expected by the unembedding layer. 

493 pos_slice: 

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

495 mlp_input: 

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

497 input rather than the attention input. 

498 return_labels: 

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

500 labelling graphs. 

501 

502 Returns: 

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

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

505 """ 

506 if not isinstance(pos_slice, Slice): 

507 pos_slice = Slice(pos_slice) 

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

509 # Default to the residual stream immediately pre unembed 

510 layer = self.model.cfg.n_layers 

511 assert isinstance(layer, int) 

512 labels = [] 

513 components_list = [] 

514 for l in range(layer + 1): 

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

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

517 labels.append("final_post") 

518 continue 

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

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

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

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

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

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

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

526 if apply_ln: 

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

528 components = self.apply_ln_to_stack( 

529 components, 

530 layer, 

531 pos_slice=pos_slice, 

532 mlp_input=mlp_input, 

533 has_batch_dim=self.has_batch_dim, 

534 recompute_ln=recompute_ln, 

535 ) 

536 if return_labels: 

537 return components, labels 

538 else: 

539 return components 

540 

541 def logit_lens(self, **kwargs: Any) -> Any: 

542 """One-call logit lens over this cache's accumulated residual stream. 

543 

544 Delegates to :func:`transformer_lens.tools.analysis.logit_lens.logit_lens` 

545 with this cache; see it for the keyword arguments (``layers``, 

546 ``positions``, ``targets``, ``top_k``, ``vocab``, ``return_type`` ...). 

547 The final stack entry reproduces the model's logits through the real 

548 final norm, ``W_U``, ``b_U`` and the adapter's output transform. 

549 """ 

550 # Lazy import: tools.analysis imports this module. 

551 from transformer_lens.tools.analysis.logit_lens import logit_lens 

552 

553 return logit_lens(self.model, cache=self, **kwargs) 

554 

555 def logit_attrs( 

556 self, 

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

558 tokens: Union[ 

559 str, 

560 int, 

561 Int[torch.Tensor, ""], 

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

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

564 ], 

565 incorrect_tokens: Optional[ 

566 Union[ 

567 str, 

568 int, 

569 Int[torch.Tensor, ""], 

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

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

572 ] 

573 ] = None, 

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

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

576 has_batch_dim: bool = True, 

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

578 """Logit Attributions. 

579 

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

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

582 

583 It does this by: 

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

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

586 directions. 

587 

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

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

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

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

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

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

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

595 

596 Warning: 

597 

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

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

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

601 

602 Args: 

603 residual_stack: 

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

605 tokens: 

606 Tokens to compute logit attributions on. A [batch] tensor selects one token 

607 per example across all retained positions. A 1-D tensor whose length differs 

608 from the batch size selects one token per position, shared across examples. 

609 Use [1, position] to share per-position targets when batch and position sizes 

610 are equal; [batch, position] selects targets for each example and position. 

611 Token tensors should already match any batch/position slices. 

612 incorrect_tokens: 

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

614 incorrect_tokens. Must have the same shape as tokens. 

615 pos_slice: 

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

617 batch_slice: 

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

619 None, do nothing. 

620 has_batch_dim: 

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

622 

623 Returns: 

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

625 was provided. 

626 """ 

627 if not isinstance(pos_slice, Slice): 

628 pos_slice = Slice(pos_slice) 

629 

630 if not isinstance(batch_slice, Slice): 

631 batch_slice = Slice(batch_slice) 

632 

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

634 tokens_for_shape_check = tokens 

635 

636 if isinstance(tokens_for_shape_check, str): 

637 tokens_for_shape_check = torch.as_tensor( 

638 self.model.to_single_token(tokens_for_shape_check) 

639 ) 

640 elif isinstance(tokens_for_shape_check, int): 

641 tokens_for_shape_check = torch.as_tensor(tokens_for_shape_check) 

642 

643 logit_directions = self.model.tokens_to_residual_directions(tokens) 

644 

645 if incorrect_tokens is not None: 

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

647 incorrect_tokens_for_shape_check = incorrect_tokens 

648 

649 if isinstance(incorrect_tokens_for_shape_check, str): 

650 incorrect_tokens_for_shape_check = torch.as_tensor( 

651 self.model.to_single_token(incorrect_tokens_for_shape_check) 

652 ) 

653 elif isinstance(incorrect_tokens_for_shape_check, int): 

654 incorrect_tokens_for_shape_check = torch.as_tensor(incorrect_tokens_for_shape_check) 

655 

656 if tokens_for_shape_check.shape != incorrect_tokens_for_shape_check.shape: 

657 raise ValueError( 

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

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

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

661 ) 

662 

663 # If incorrect_tokens was provided, take the logit difference 

664 logit_directions = logit_directions - self.model.tokens_to_residual_directions( 

665 incorrect_tokens 

666 ) 

667 

668 scaled_residual_stack = self.apply_ln_to_stack( 

669 residual_stack, 

670 layer=-1, 

671 pos_slice=pos_slice, 

672 batch_slice=batch_slice, 

673 has_batch_dim=has_batch_dim, 

674 ) 

675 

676 if ( 

677 has_batch_dim 

678 and batch_slice.mode != "int" 

679 and scaled_residual_stack.ndim == 4 

680 and logit_directions.ndim == 2 

681 and logit_directions.shape[0] == scaled_residual_stack.shape[1] 

682 ): 

683 # Per-example directions must broadcast over positions, not across the batch. 

684 logit_directions = logit_directions.unsqueeze(-2) 

685 

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

687 return logit_attrs 

688 

689 def decompose_resid( 

690 self, 

691 layer: Optional[int] = None, 

692 mlp_input: bool = False, 

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

694 apply_ln: bool = False, 

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

696 incl_embeds: bool = True, 

697 return_labels: bool = False, 

698 ) -> Union[ 

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

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

701 ]: 

702 """Decompose the Residual Stream. 

703 

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

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

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

707 

708 Args: 

709 layer: 

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

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

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

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

714 accumulated streams up to the input to layer l 

715 mlp_input: 

716 Whether to include attn_out for the current 

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

718 rather than the Attn input. 

719 mode: 

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

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

722 attention components. Defaults to "all". 

723 apply_ln: 

724 Whether to apply LayerNorm to the stack. 

725 pos_slice: 

726 A slice object to apply to the pos dimension. 

727 Defaults to None, do nothing. 

728 incl_embeds: 

729 Whether to include embed & pos_embed, whose hooks expose their 

730 contributions to the residual stream. 

731 return_labels: 

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

733 Useful for labelling graphs. 

734 

735 Returns: 

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

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

738 """ 

739 if not isinstance(pos_slice, Slice): 

740 pos_slice = Slice(pos_slice) 

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

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

743 # Default to the residual stream immediately pre unembed 

744 layer = self.model.cfg.n_layers 

745 assert isinstance(layer, int) 

746 

747 incl_attn = mode != "mlp" 

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

749 components_list = [] 

750 labels = [] 

751 if incl_embeds: 

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

753 components_list = [self["hook_embed"]] 

754 labels.append("embed") 

755 if self.has_pos_embed: 

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

757 labels.append("pos_embed") 

758 

759 for l in range(layer): 

760 if incl_attn: 

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

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

763 if incl_mlp: 

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

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

766 if mlp_input and incl_attn: 

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

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

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

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

771 if apply_ln: 

772 components = self.apply_ln_to_stack( 

773 components, layer, pos_slice=pos_slice, mlp_input=mlp_input 

774 ) 

775 if return_labels: 

776 return components, labels 

777 else: 

778 return components 

779 

780 def compute_head_results( 

781 self, 

782 ): 

783 """Compute Head Results. 

784 

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

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

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

788 be useful if you forget. 

789 

790 Existing per-head results are preserved, including those without a batch dimension. 

791 Missing or invalid results are recomputed from cached ``hook_z`` and the model's 

792 ``W_O`` weights. A per-head contraction avoids materializing the broadcast product 

793 over both ``d_head`` and ``d_model``. As in the bridge forward pass, the contraction 

794 follows active autocast settings. 

795 

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

797 compatibility shim. 

798 """ 

799 if self._ensure_head_results(): 

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

801 

802 def _ensure_head_results(self) -> bool: 

803 """Recompute missing or invalid per-head results; True if all were already cached.""" 

804 expected_ndim = 4 if self.has_batch_dim else 3 

805 all_cached = True 

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

807 result_key = f"blocks.{layer}.attn.hook_result" 

808 cached_result = self.cache_dict.get(result_key) 

809 # Batchless per-head results are valid 3D tensors. 

810 if ( 

811 isinstance(cached_result, torch.Tensor) 

812 and cached_result.ndim == expected_ndim 

813 and cached_result.shape[-2] == self.model.cfg.n_heads 

814 ): 

815 continue 

816 all_cached = False 

817 

818 z = self[("z", layer, "attn")] 

819 weights = self.model.blocks[layer].attn.W_O 

820 if z.dtype != weights.dtype: 

821 # Preserve broadcast promotion for caches collected under autocast. 

822 dtype = torch.promote_types(z.dtype, weights.dtype) 

823 z, weights = z.to(dtype), weights.to(dtype) 

824 result = torch.einsum("...hd,hdm->...hm", z, weights) 

825 scale = getattr(self.model.blocks[layer].attn, "_result_contribution_scale", 1.0) 

826 self.cache_dict[result_key] = result if scale == 1.0 else result * scale 

827 return all_cached 

828 

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

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

831 

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

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

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

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

836 ``cfg.layers_block_type``. 

837 

838 Args: 

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

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

841 

842 Returns: 

843 Ascending list of SSM block indices. 

844 """ 

845 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

846 find_ssm_mixer, 

847 ) 

848 

849 bridge = self.model 

850 layers: List[int] = [] 

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

852 mixer = find_ssm_mixer(block) 

853 if mixer is None: 

854 continue 

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

856 continue 

857 layers.append(i) 

858 return layers 

859 

860 def _over_ssm_layers( 

861 self, 

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

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

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

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

866 

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

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

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

870 dict over the SSM layers (heterogeneous hybrids). 

871 """ 

872 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

873 find_ssm_mixer, 

874 ) 

875 

876 bridge = self.model 

877 indices = self.ssm_layers(mixer_type=mixer_type) 

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

879 raise RuntimeError( 

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

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

882 ) 

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

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

885 } 

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

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

888 return results 

889 

890 def compute_ssm_effective_attention( 

891 self, 

892 layer: Optional[int] = None, 

893 include_dt_scaling: bool = False, 

894 per_state_coord: bool = False, 

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

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

897 

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

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

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

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

902 

903 Args: 

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

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

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

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

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

909 

910 Returns: 

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

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

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

914 

915 Raises: 

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

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

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

919 """ 

920 import inspect 

921 

922 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

923 find_ssm_mixer, 

924 ) 

925 

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

927 fn = cast(Any, mixer).compute_effective_attention 

928 params = inspect.signature(fn).parameters 

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

930 for name, value in ( 

931 ("include_dt_scaling", include_dt_scaling), 

932 ("per_state_coord", per_state_coord), 

933 ): 

934 if name in params: 

935 kwargs[name] = value 

936 elif value: 

937 raise ValueError( 

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

939 f"support {name}=True." 

940 ) 

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

942 return result 

943 

944 if layer is not None: 

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

946 if single is None: 

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

948 return _call(single, layer) 

949 return self._over_ssm_layers(_call) 

950 

951 def compute_ssm_state( 

952 self, 

953 layer: Optional[int] = None, 

954 time_step: Optional[int] = None, 

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

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

957 

958 The single discovery surface for recurrent state across families — 

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

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

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

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

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

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

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

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

967 shares one mixer type. 

968 

969 Args: 

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

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

972 

973 Returns: 

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

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

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

977 

978 Raises: 

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

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

981 """ 

982 from transformer_lens.model_bridge.generalized_components import ( 

983 GatedDeltaNetBridge, 

984 SSM2MixerBridge, 

985 SSMMixerBridge, 

986 ) 

987 from transformer_lens.model_bridge.generalized_components.ssm_protocol import ( 

988 find_ssm_mixer, 

989 ) 

990 

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

992 state_mixers = (SSMMixerBridge, SSM2MixerBridge, GatedDeltaNetBridge) 

993 

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

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

996 self, layer_idx=layer_idx, time_step=time_step 

997 ) 

998 return state 

999 

1000 if layer is not None: 

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

1002 if not isinstance(single, state_mixers): 

1003 raise TypeError( 

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

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

1006 ) 

1007 return _call(single, layer) 

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

1009 

1010 def stack_head_results( 

1011 self, 

1012 layer: int = -1, 

1013 return_labels: bool = False, 

1014 incl_remainder: bool = False, 

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

1016 apply_ln: bool = False, 

1017 ) -> Union[ 

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

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

1020 ]: 

1021 """Stack Head Results. 

1022 

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

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

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

1026 notation). 

1027 

1028 Args: 

1029 layer: 

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

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

1032 return_labels: 

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

1034 incl_remainder: 

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

1036 pos_slice: 

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

1038 apply_ln: 

1039 Whether to apply LayerNorm to the stack. 

1040 """ 

1041 if not isinstance(pos_slice, Slice): 

1042 pos_slice = Slice(pos_slice) 

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

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

1045 # Default to the residual stream immediately pre unembed 

1046 layer = self.model.cfg.n_layers 

1047 

1048 # Idempotent; cleans up stale Bridge entries without the 'already cached' warning, 

1049 # which is only meaningful for direct compute_head_results callers. 

1050 self._ensure_head_results() 

1051 

1052 components: Any = [] 

1053 labels = [] 

1054 for l in range(layer): 

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

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

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

1058 if components: 

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

1060 components = einops.rearrange( 

1061 components, 

1062 "... concat_head_index d_model -> concat_head_index ... d_model", 

1063 ) 

1064 if incl_remainder: 

1065 remainder = pos_slice.apply( 

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

1067 ) - components.sum(dim=0) 

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

1069 labels.append("remainder") 

1070 elif incl_remainder: 

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

1072 components = torch.cat( 

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

1074 ) 

1075 labels.append("remainder") 

1076 else: 

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

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

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

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

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

1082 components = torch.zeros( 

1083 0, 

1084 *empty_src.shape, 

1085 dtype=empty_src.dtype, 

1086 device=empty_src.device, 

1087 ) 

1088 

1089 if apply_ln: 

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

1091 

1092 if return_labels: 

1093 return components, labels 

1094 else: 

1095 return components 

1096 

1097 def stack_activation( 

1098 self, 

1099 activation_name: str, 

1100 layer: int = -1, 

1101 sublayer_type: Optional[str] = None, 

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

1103 """Stack Activations. 

1104 

1105 Flexible way to stack activations with a given name. 

1106 

1107 Args: 

1108 activation_name: 

1109 The name of the activation to be stacked 

1110 layer: 

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

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

1113 sublayer_type: 

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

1115 inferred. 

1116 incl_remainder: 

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

1118 """ 

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

1120 # Default to the residual stream immediately pre unembed 

1121 layer = self.model.cfg.n_layers 

1122 

1123 components = [] 

1124 for l in range(layer): 

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

1126 

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

1128 

1129 def get_neuron_results( 

1130 self, 

1131 layer: int, 

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

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

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

1135 ) -> torch.Tensor: 

1136 """Get Neuron Results. 

1137 

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

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

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

1141 

1142 Args: 

1143 layer: 

1144 Layer index. 

1145 neuron_slice: 

1146 Slice of the neuron. 

1147 pos_slice: 

1148 Slice of the positions. 

1149 project_output_onto: 

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

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

1152 intermediate is never materialized. 

1153 

1154 Returns: 

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

1156 (1D projection). 

1157 """ 

1158 if not isinstance(neuron_slice, Slice): 

1159 neuron_slice = Slice(neuron_slice) 

1160 if not isinstance(pos_slice, Slice): 

1161 pos_slice = Slice(pos_slice) 

1162 

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

1164 block = self.model.blocks[layer] 

1165 W_out = block.mlp.W_out 

1166 contribution_scale = getattr(block, "residual_contribution_scale", 1.0) 

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

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

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

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

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

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

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

1174 if contribution_scale != 1.0: 

1175 neuron_acts = neuron_acts * contribution_scale 

1176 if project_output_onto is None: 

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

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

1179 projected = W_out @ project_output_onto 

1180 if project_output_onto.ndim == 1: 

1181 return neuron_acts * projected 

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

1183 

1184 def _uses_ln_centering(self, layer: Optional[int]) -> bool: 

1185 """Use the target norm's semantics, not just the block normalization family.""" 

1186 is_final = layer is None or layer == self.model.cfg.n_layers 

1187 return self.model.cfg.normalization_type in ["LN", "LNPre"] and not ( 

1188 is_final and getattr(self.model.cfg, "final_rms", False) 

1189 ) 

1190 

1191 def _get_cached_ln_scale( 

1192 self, 

1193 layer: Optional[int], 

1194 mlp_input: bool, 

1195 pos_slice: Slice, 

1196 batch_slice: Optional[Slice] = None, 

1197 ) -> torch.Tensor: 

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

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

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

1201 """ 

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

1203 key = "ln_final.hook_scale" 

1204 else: 

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

1206 try: 

1207 scale = self[key] 

1208 except KeyError as e: 

1209 norm = f"ln{2 if mlp_input else 1}" 

1210 blocks = getattr(self.model, "blocks", None) 

1211 block = blocks[layer] if blocks is not None and layer is not None else None 

1212 if ( 1212 ↛ 1223line 1212 didn't jump to line 1223 because the condition on line 1212 was always true

1213 block is not None 

1214 and getattr(block, norm, None) is None 

1215 and getattr(block, f"{norm}_post", None) is not None 

1216 ): 

1217 sublayer = "MLP" if mlp_input else "attention" 

1218 raise KeyError( 

1219 f"Layer {layer} has no {norm} (pre-{sublayer} norm): this architecture " 

1220 f"normalizes sublayer outputs ({norm}_post) instead, so a mid-layer cached-scale " 

1221 f"LN is undefined. Use apply_ln=False, or apply LN at the final layer." 

1222 ) from e 

1223 raise KeyError( 

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

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

1226 f"under different module paths)." 

1227 ) from e 

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

1229 if batch_slice is not None and self.has_batch_dim: 

1230 scale = batch_slice.apply(scale) 

1231 return scale 

1232 

1233 def _stack_neuron_results_apply_ln_projected( 

1234 self, 

1235 layer: int, 

1236 pos_slice: Slice, 

1237 neuron_slice: Slice, 

1238 project_2d: torch.Tensor, 

1239 mlp_input: bool = False, 

1240 ) -> torch.Tensor: 

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

1242 

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

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

1245 RMS models drop the ``mean(W_out_n) * sum_p`` term (no centering). ``s`` is the ln2 

1246 scale of ``layer`` when ``mlp_input`` is set, else the ln1 scale. 

1247 """ 

1248 scale = self._get_cached_ln_scale(layer, mlp_input=mlp_input, pos_slice=pos_slice) 

1249 

1250 apply_centering = self._uses_ln_centering(layer) 

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

1252 

1253 components: list = [] 

1254 for l in range(layer): 

1255 block = self.model.blocks[l] 

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

1257 contribution_scale = getattr(block, "residual_contribution_scale", 1.0) 

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

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

1260 if apply_centering: 

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

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

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

1264 else: 

1265 lin_form_l = W_proj_l 

1266 if contribution_scale != 1.0: 

1267 lin_form_l = lin_form_l * contribution_scale 

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

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

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

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

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

1273 if not components: 

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

1275 return torch.zeros( 

1276 0, 

1277 *empty_src.shape[:-1], 

1278 project_2d.shape[-1], 

1279 dtype=torch.promote_types( 

1280 torch.promote_types(empty_src.dtype, scale.dtype), project_2d.dtype 

1281 ), 

1282 device=empty_src.device, 

1283 ) 

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

1285 return einops.rearrange( 

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

1287 ) 

1288 

1289 def stack_neuron_results( 

1290 self, 

1291 layer: int, 

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

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

1294 return_labels: bool = False, 

1295 incl_remainder: bool = False, 

1296 apply_ln: bool = False, 

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

1298 mlp_input: bool = False, 

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

1300 """Stack Neuron Results 

1301 

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

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

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

1305 into attribution by specific neurons. 

1306 

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

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

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

1310 

1311 Args: 

1312 layer: 

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

1314 in [1, n_layers]. ``layer=0`` follows the negative-index convention: the neuron 

1315 stack is empty and with ``incl_remainder=True`` the remainder is the final layer's 

1316 residual stream (``resid_post`` of layer ``n_layers - 1``), i.e. the whole model. 

1317 pos_slice: 

1318 Slice of the positions. 

1319 neuron_slice: 

1320 Slice of the neurons. An int or an integer-mode ``Slice(n)`` selects a single 

1321 neuron and is treated like ``[n]``. 

1322 return_labels: 

1323 Whether to also return a list of labels of the form "L0N0" for the neurons. 

1324 incl_remainder: 

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

1326 apply_ln: 

1327 Whether to apply LayerNorm to the stack. 

1328 project_output_onto: 

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

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

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

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

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

1334 mlp_input: 

1335 Treat the stack as the input to ``layer``'s MLP rather than its attention, as in 

1336 ``decompose_resid``: with ``incl_remainder=True`` the remainder fills the stack up to 

1337 ``resid_mid`` instead of ``resid_pre``, and with ``apply_ln=True`` the LN scale of 

1338 ln2 is used instead of ln1. The neurons stacked are unchanged. 

1339 """ 

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

1341 # Default to the residual stream immediately pre unembed 

1342 layer = self.model.cfg.n_layers 

1343 

1344 # layer=0 is the documented whole-model case (remainder reads resid_post of layer -1, the 

1345 # final layer). Anything below it would wrap the remainder target to an arbitrary late 

1346 # layer via __getitem__, silently returning the wrong residual stream. 

1347 if incl_remainder and layer < 0: 

1348 raise ValueError( 

1349 f"layer must be >= 0 with incl_remainder=True (-1/None mean n_layers); got {layer}." 

1350 ) 

1351 

1352 # Layers are concatenated along the neuron axis, so it must not collapse. unwrap turns an 

1353 # int into [n] but leaves a Slice as is, so an integer-mode Slice(n) is rebuilt as [n] 

1354 # here. A new Slice is made, so the caller's Slice still collapses elsewhere. 

1355 neuron_slice = Slice.unwrap(neuron_slice) 

1356 if neuron_slice.mode == "int": 

1357 assert isinstance(neuron_slice.slice, int) # narrow for mypy 

1358 neuron_slice = Slice([neuron_slice.slice]) 

1359 if not isinstance(pos_slice, Slice): 

1360 pos_slice = Slice(pos_slice) 

1361 

1362 project_2d, squeeze_projected = _normalize_projection_to_2d(project_output_onto) 

1363 

1364 d_mlp = self.model.cfg.d_mlp 

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

1366 neuron_labels = neuron_slice.apply(torch.arange(d_mlp), dim=0) 

1367 

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

1369 # The remainder fills the stack up to the residual stream that layer's LN normalizes: 

1370 # resid_mid (the MLP input) with mlp_input, else resid_pre (resid_post of layer - 1). 

1371 remainder_target = ("resid_mid", layer) if mlp_input else ("resid_post", layer - 1) 

1372 components: Any 

1373 ln_folded = apply_ln and project_2d is not None 

1374 if ln_folded: 

1375 assert project_2d is not None # narrow for mypy 

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

1377 components = self._stack_neuron_results_apply_ln_projected( 

1378 layer, pos_slice, neuron_slice, project_2d, mlp_input=mlp_input 

1379 ) 

1380 if incl_remainder: 

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

1382 target = pos_slice.apply(self[remainder_target], dim=-2) 

1383 target_ln = self.apply_ln_to_stack( 

1384 target[None], layer, pos_slice=pos_slice, mlp_input=mlp_input 

1385 )[0] 

1386 remainder = target_ln @ project_2d 

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

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

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

1390 labels.append("remainder") 

1391 else: 

1392 per_layer: list = [] 

1393 for l in range(layer): 

1394 per_layer.append( 

1395 self.get_neuron_results( 

1396 l, 

1397 pos_slice=pos_slice, 

1398 neuron_slice=neuron_slice, 

1399 project_output_onto=project_2d, 

1400 ) 

1401 ) 

1402 if per_layer: 

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

1404 components = einops.rearrange( 

1405 components, 

1406 "... concat_neuron_index d_model -> concat_neuron_index ... d_model", 

1407 ) 

1408 if incl_remainder: 

1409 remainder_full = pos_slice.apply(self[remainder_target], dim=-2) 

1410 if project_2d is not None: 

1411 remainder_full = remainder_full @ project_2d 

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

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

1414 labels.append("remainder") 

1415 elif incl_remainder: 

1416 remainder_full = pos_slice.apply(self[remainder_target], dim=-2) 

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

1418 remainder_full = remainder_full @ project_2d 

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

1420 labels.append("remainder") 

1421 else: 

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

1423 empty_shape = tuple(empty_src.shape) 

1424 empty_dtype = empty_src.dtype 

1425 if project_2d is not None: 

1426 # Shape only: a real matmul would demand the cache and direction share a dtype. 

1427 empty_shape = (*empty_shape[:-1], project_2d.shape[-1]) 

1428 empty_dtype = torch.promote_types(empty_dtype, project_2d.dtype) 

1429 components = torch.zeros( 

1430 0, *empty_shape, dtype=empty_dtype, device=empty_src.device 

1431 ) 

1432 

1433 if apply_ln: 

1434 components = self.apply_ln_to_stack( 

1435 components, layer, pos_slice=pos_slice, mlp_input=mlp_input 

1436 ) 

1437 

1438 if squeeze_projected: 

1439 components = components.squeeze(-1) 

1440 

1441 if return_labels: 

1442 return components, labels 

1443 else: 

1444 return components 

1445 

1446 def apply_ln_to_stack( 

1447 self, 

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

1449 layer: Optional[int] = None, 

1450 mlp_input: bool = False, 

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

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

1453 has_batch_dim: bool = True, 

1454 recompute_ln: bool = False, 

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

1456 """Apply Layer Norm to a Stack. 

1457 

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

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

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

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

1462 

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

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

1465 applying a new LayerNorm. 

1466 

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

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

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

1470 

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

1472 

1473 Args: 

1474 residual_stack: 

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

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

1477 or position dimensions. 

1478 layer: 

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

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

1481 mlp_input: 

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

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

1484 pos_slice: 

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

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

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

1488 Defaults to None, do nothing. 

1489 batch_slice: 

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

1491 has_batch_dim: 

1492 Whether residual_stack has a batch dimension. 

1493 recompute_ln: 

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

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

1496 

1497 """ 

1498 if not isinstance(pos_slice, Slice): 

1499 pos_slice = Slice(pos_slice) 

1500 if not isinstance(batch_slice, Slice): 

1501 batch_slice = Slice(batch_slice) 

1502 

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

1504 # Default to the residual stream immediately pre unembed 

1505 layer = self.model.cfg.n_layers 

1506 

1507 if has_batch_dim: 

1508 # Apply batch slice to the stack 

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

1510 

1511 # Checked only after slicing so callers like logit_attrs still get their 

1512 # batch_slice honoured on models without LayerNorm/RMSNorm. 

1513 if self.model.cfg.normalization_type not in ["LN", "LNPre", "RMS", "RMSPre"]: 

1514 return residual_stack 

1515 

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

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

1518 ln_final = self.model.ln_final 

1519 results = [] 

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

1521 x = residual_stack[i] 

1522 original_shape = x.shape 

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

1524 if not has_batch_dim: 

1525 x = x.unsqueeze(0) 

1526 if x.ndim == 2: 

1527 x = x.unsqueeze(1) 

1528 out = ln_final(x) 

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

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

1531 

1532 # Final RMSNorm must not inherit the block LayerNorm centering convention. 

1533 if self._uses_ln_centering(layer): 

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

1535 

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

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

1538 

1539 return residual_stack / scale 

1540 

1541 def get_full_resid_decomposition( 

1542 self, 

1543 layer: Optional[int] = None, 

1544 mlp_input: bool = False, 

1545 expand_neurons: bool = True, 

1546 apply_ln: bool = False, 

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

1548 return_labels: bool = False, 

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

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

1551 """Get the full Residual Decomposition. 

1552 

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

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

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

1556 

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

1558 

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

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

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

1562 else ``L`` rows) 

1563 3. ``embed`` (1 row, including any post-embedding runtime multiplier) 

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

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

1566 

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

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

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

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

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

1572 

1573 Args: 

1574 layer: 

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

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

1577 just embed and pos_embed 

1578 mlp_input: 

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

1580 layer, since that's the unembed. 

1581 expand_neurons: 

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

1583 MLP layer outputs. 

1584 apply_ln: 

1585 Whether to apply LayerNorm to the stack. 

1586 pos_slice: 

1587 Slice of the positions to take. 

1588 return_labels: 

1589 Whether to return the labels. 

1590 project_output_onto: 

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

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

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

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

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

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

1597 """ 

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

1599 # Default to the residual stream immediately pre unembed 

1600 layer = self.model.cfg.n_layers 

1601 assert layer is not None # keep mypy happy 

1602 

1603 if not isinstance(pos_slice, Slice): 

1604 pos_slice = Slice(pos_slice) 

1605 

1606 project_2d, squeeze_projected = _normalize_projection_to_2d(project_output_onto) 

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

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

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

1610 ln_folded = apply_ln and project_2d is not None 

1611 

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

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

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

1615 

1616 head_stack, head_labels = self.stack_head_results( 

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

1618 ) 

1619 if ln_folded: 

1620 head_stack = _ln_then_project(head_stack) 

1621 elif project_2d is not None: 

1622 head_stack = head_stack @ project_2d 

1623 labels = head_labels 

1624 components = [head_stack] 

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

1626 if expand_neurons: 

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

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

1629 neuron_stack, neuron_labels = self.stack_neuron_results( 

1630 layer, 

1631 pos_slice=pos_slice, 

1632 return_labels=True, 

1633 apply_ln=ln_folded, 

1634 project_output_onto=project_2d, 

1635 mlp_input=mlp_input, 

1636 ) 

1637 labels.extend(neuron_labels) 

1638 components.append(neuron_stack) 

1639 else: 

1640 # Get the stack of just the MLP outputs 

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

1642 # just for MLP outputs 

1643 mlp_stack, mlp_labels = self.decompose_resid( 

1644 layer, 

1645 mlp_input=mlp_input, 

1646 pos_slice=pos_slice, 

1647 incl_embeds=False, 

1648 mode="mlp", 

1649 return_labels=True, 

1650 ) 

1651 if ln_folded: 

1652 mlp_stack = _ln_then_project(mlp_stack) 

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

1654 mlp_stack = mlp_stack @ project_2d 

1655 labels.extend(mlp_labels) 

1656 components.append(mlp_stack) 

1657 

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

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

1660 if ln_folded: 

1661 embed = _ln_then_project(embed) 

1662 elif project_2d is not None: 

1663 embed = embed @ project_2d 

1664 labels.append("embed") 

1665 components.append(embed) 

1666 if self.has_pos_embed: 

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

1668 if ln_folded: 

1669 pos_embed = _ln_then_project(pos_embed) 

1670 elif project_2d is not None: 

1671 pos_embed = pos_embed @ project_2d 

1672 labels.append("pos_embed") 

1673 components.append(pos_embed) 

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

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

1676 if ln_folded: 

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

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

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

1680 else: 

1681 if project_2d is not None: 

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

1683 bias_full = bias_full @ project_2d 

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

1685 labels.append("bias") 

1686 components.append(bias) 

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

1688 if apply_ln and not ln_folded: 

1689 residual_stack = self.apply_ln_to_stack( 

1690 residual_stack, layer, pos_slice=pos_slice, mlp_input=mlp_input 

1691 ) 

1692 

1693 if squeeze_projected: 

1694 residual_stack = residual_stack.squeeze(-1) 

1695 

1696 if return_labels: 

1697 return residual_stack, labels 

1698 else: 

1699 return residual_stack