Coverage for transformer_lens/model_bridge/generalized_components/opaque_block.py: 67%
54 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-08-11 18:50 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-08-11 18:50 +0000
1"""Generic opaque block bridge for non-SSM, non-standard-transformer architectures."""
3from __future__ import annotations
5import re
6from typing import Any, Dict, Optional
8import torch
10from transformer_lens.model_bridge.exceptions import StopAtLayerException
11from transformer_lens.model_bridge.generalized_components.base import (
12 GeneralizedComponent,
13)
16class OpaqueBlockBridge(GeneralizedComponent):
17 """Generic block bridge that delegates a full block forward unchanged.
19 Exposes ``hook_resid_pre`` / ``hook_resid_post`` (aliases of ``hook_in``
20 / ``hook_out``) on the residual stream only — no assumptions are made
21 about the block's internal structure.
23 Use this for architectures whose block internals do not follow a standard
24 SSM or transformer pre-norm flow, e.g.:
26 - Post-residual (sandwich-norm) blocks like Raven / Huginn.
27 - Attention-free recurrent blocks like RWKV-7.
28 - Any custom block where ``hook_mixer_in`` / ``hook_mixer_out`` would be
29 semantically wrong or structurally absent.
31 For SSM architectures (Mamba, Falcon-H1) use :class:`SSMBlockBridge`,
32 which extends this class and adds the ``hook_mixer_in`` /
33 ``hook_mixer_out`` aliases pointing at ``mixer.hook_in`` /
34 ``mixer.hook_out``.
35 """
37 is_list_item: bool = True
38 hook_out_is_single_residual_stream: bool = True
39 hook_aliases = {
40 "hook_resid_pre": "hook_in",
41 "hook_resid_post": "hook_out",
42 }
44 def __init__(
45 self,
46 name: str,
47 config: Optional[Any] = None,
48 submodules: Optional[Dict[str, GeneralizedComponent]] = None,
49 hook_alias_overrides: Optional[Dict[str, str]] = None,
50 ):
51 super().__init__(
52 name,
53 config,
54 submodules=submodules if submodules is not None else {},
55 hook_alias_overrides=hook_alias_overrides,
56 )
58 def forward(self, *args: Any, **kwargs: Any) -> Any:
59 """Delegate to the HF block with hook_in/hook_out wrapped around it."""
60 if self.original_component is None: 60 ↛ 61line 60 didn't jump to line 61 because the condition on line 60 was never true
61 raise RuntimeError(
62 f"Original component not set for {self.name}. "
63 "Call set_original_component() first."
64 )
66 self._check_stop_at_layer(*args, **kwargs)
67 args, kwargs = self._hook_input_hidden_states(args, kwargs)
68 output = self.original_component(*args, **kwargs)
69 return self._apply_output_hook(output)
71 def _apply_output_hook(self, output: Any) -> Any:
72 """Hook the primary output tensor, preserving tuple structure if present."""
73 if isinstance(output, tuple) and len(output) > 0: 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true
74 first = output[0]
75 if isinstance(first, torch.Tensor):
76 first = self.hook_out(first)
77 return (first,) + output[1:]
78 return output
79 if isinstance(output, torch.Tensor): 79 ↛ 81line 79 didn't jump to line 81 because the condition on line 79 was always true
80 return self.hook_out(output)
81 return output
83 def _hook_input_hidden_states(self, args: tuple, kwargs: dict) -> tuple[tuple, dict]:
84 """Hook the hidden_states input whether it arrives positionally or by name."""
85 if len(args) > 0 and isinstance(args[0], torch.Tensor): 85 ↛ 88line 85 didn't jump to line 88 because the condition on line 85 was always true
86 hooked = self.hook_in(args[0])
87 args = (hooked,) + args[1:]
88 elif "hidden_states" in kwargs and isinstance(kwargs["hidden_states"], torch.Tensor):
89 kwargs["hidden_states"] = self.hook_in(kwargs["hidden_states"])
90 return args, kwargs
92 def _check_stop_at_layer(self, *args: Any, **kwargs: Any) -> None:
93 """Raise StopAtLayerException when the configured stop index matches this block."""
94 if not (hasattr(self, "_stop_at_layer_idx") and self._stop_at_layer_idx is not None):
95 return
96 if self.name is None: 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true
97 return
98 # Mamba uses `.layers.{i}`; `blocks.{i}` is the fallback TL convention.
99 match = re.search(r"\.layers\.(\d+)", self.name) or re.search(r"blocks\.(\d+)", self.name)
100 if not match: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 return
102 layer_idx = int(match.group(1))
103 if layer_idx != self._stop_at_layer_idx:
104 return
105 if len(args) > 0 and isinstance(args[0], torch.Tensor): 105 ↛ 107line 105 didn't jump to line 107 because the condition on line 105 was always true
106 input_tensor = args[0]
107 elif "hidden_states" in kwargs and isinstance(kwargs["hidden_states"], torch.Tensor):
108 input_tensor = kwargs["hidden_states"]
109 else:
110 raise ValueError(f"Cannot find input tensor to stop at layer {layer_idx}")
111 input_tensor = self.hook_in(input_tensor)
112 raise StopAtLayerException(input_tensor)