Coverage for transformer_lens/model_bridge/generalized_components/unembedding.py: 84%
93 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-10-10 05:10 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-10-10 05:10 +0000
1"""Unembedding bridge component.
3This module contains the bridge component for unembedding layers.
4"""
5from collections import ChainMap
6from typing import Any, Dict, Optional, cast
8import torch
9from accelerate.hooks import AlignDevicesHook, SequentialHook
10from accelerate.utils import align_module_device
12from transformer_lens.model_bridge.generalized_components.base import (
13 GeneralizedComponent,
14)
17class UnembeddingBridge(GeneralizedComponent):
18 """Unembedding bridge that wraps transformer unembedding layers.
20 This component provides standardized input/output hooks.
21 """
23 property_aliases = {"W_U": "u.weight"}
25 def __init__(
26 self,
27 name: str,
28 config: Optional[Any] = None,
29 submodules: Optional[Dict[str, GeneralizedComponent]] = {},
30 ):
31 """Initialize the unembedding bridge.
33 Args:
34 name: The name of this component
35 config: Optional configuration (unused for UnembeddingBridge)
36 submodules: Dictionary of GeneralizedComponent submodules to register
37 """
38 super().__init__(name, config, submodules=submodules)
40 def set_original_component(self, original_component: torch.nn.Module) -> None:
41 """Set the original component with a fixed zero bias when the checkpoint has none.
43 Args:
44 original_component: The original transformer component to wrap
45 """
46 # Keep b_U and state_dict compatibility without adding an optimizer parameter.
47 if isinstance(original_component, torch.nn.Linear) and original_component.bias is None:
48 # Get the output features (vocab size)
49 vocab_size = original_component.weight.shape[0] # shape is safe on a meta tensor too
50 dtype = original_component.weight.dtype # dtype is also safe on a meta tensor
52 device = self._bias_device(original_component)
54 del original_component.bias
55 original_component.register_buffer(
56 "bias", torch.zeros(vocab_size, device=device, dtype=dtype)
57 )
58 self._update_offloaded_bias(original_component)
60 super().set_original_component(original_component)
62 @staticmethod
63 def _offload_hook(component: torch.nn.Module) -> Optional[AlignDevicesHook]:
64 pending = [getattr(component, "_hf_hook", None)]
65 while pending:
66 hook = pending.pop()
67 if isinstance(hook, SequentialHook):
68 pending.extend(hook.hooks)
69 elif isinstance(hook, AlignDevicesHook) and hook.offload:
70 return hook
71 return None
73 @staticmethod
74 def _bias_device(component: torch.nn.Module) -> torch.device:
75 with align_module_device(component):
76 weight = component.weight
77 assert isinstance(weight, torch.Tensor)
78 device = weight.device
79 if device.type == "meta":
80 # align_module_device does not recognize SequentialHook offloading.
81 hook = UnembeddingBridge._offload_hook(component)
82 if hook is not None and hook.execution_device is not None:
83 device = torch.device(hook.execution_device)
84 return device
86 @staticmethod
87 def _update_offloaded_bias(component: torch.nn.Module) -> None:
88 hook = UnembeddingBridge._offload_hook(component)
89 if hook is not None:
90 bias = component.bias
91 assert isinstance(bias, torch.Tensor)
92 assert hook.weights_map is not None
93 # Only the first ChainMap layer is written; the offload map stays lazy.
94 hook.weights_map = ChainMap({"bias": bias.detach().cpu()}, cast(Any, hook.weights_map))
95 # Hook removal must restore the latest bias alongside its weight.
96 hook.original_devices.setdefault("bias", hook.original_devices["weight"])
98 def set_processed_weights(
99 self, weights: Dict[str, torch.Tensor], verbose: bool = False
100 ) -> None:
101 """Apply folded output biases while keeping synthetic biases non-trainable."""
102 component = self.original_component
103 if component is not None and "bias" in component._buffers and "bias" in weights:
104 bias = component._buffers["bias"]
105 new_bias = weights["bias"]
106 if bias is not None and bias.shape != new_bias.shape:
107 raise ValueError(
108 f"Shape mismatch when setting weight 'bias' in {type(component).__name__}: "
109 f"existing bias shape {bias.shape} != new tensor shape {new_bias.shape}"
110 )
111 component.bias = new_bias.detach()
112 self._update_offloaded_bias(component)
113 super().set_processed_weights(weights, verbose=verbose)
115 @property
116 def W_U(self) -> torch.Tensor:
117 """Return the unembedding weight matrix in TL format [d_model, d_vocab]."""
118 if "_processed_W_U" in self._parameters: 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true
119 processed_W_U = self._parameters["_processed_W_U"]
120 if processed_W_U is not None:
121 # Processed weights are in HF format [vocab, d_model]
122 # Transpose to TL format [d_model, d_vocab]
123 return processed_W_U.T
124 if self.original_component is None: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true
125 raise RuntimeError(f"Original component not set for {self.name}")
126 assert hasattr(
127 self.original_component, "weight"
128 ), f"Component {self.name} has no weight attribute"
129 weight = self.original_component.weight
130 assert isinstance(weight, torch.Tensor), f"Weight is not a tensor for {self.name}"
131 # HF format is [d_vocab, d_model], transpose to TL format [d_model, d_vocab]
132 return weight.T
134 def forward(self, hidden_states: torch.Tensor, **kwargs: Any) -> torch.Tensor:
135 """Forward pass through the unembedding bridge.
137 Args:
138 hidden_states: Input hidden states
139 **kwargs: Additional arguments to pass to the original component
141 Returns:
142 Unembedded output (logits)
143 """
144 if self.original_component is None: 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true
145 raise RuntimeError(
146 f"Original component not set for {self.name}. Call set_original_component() first."
147 )
148 hidden_states = self.hook_in(hidden_states)
149 output = self.original_component(hidden_states, **kwargs)
151 output = self.hook_out(output)
152 return output
154 @property
155 def b_U(self) -> torch.Tensor:
156 """Access the unembedding bias vector."""
157 if self.original_component is None: 157 ↛ 158line 157 didn't jump to line 158 because the condition on line 157 was never true
158 raise RuntimeError(f"Original component not set for {self.name}")
159 if hasattr(self.original_component, "bias") and self.original_component.bias is not None: 159 ↛ 164line 159 didn't jump to line 164 because the condition on line 159 was always true
160 bias = self.original_component.bias
161 assert isinstance(bias, torch.Tensor), f"Bias is not a tensor for {self.name}"
162 return bias
163 else:
164 assert hasattr(
165 self.original_component, "weight"
166 ), f"Component {self.name} has no weight attribute"
167 weight = self.original_component.weight
168 assert isinstance(weight, torch.Tensor), f"Weight is not a tensor for {self.name}"
169 dtype = weight.dtype # safe on a meta tensor
170 vocab_size: int = int(weight.shape[0]) # safe on a meta tensor
171 device = self._bias_device(self.original_component)
172 return torch.zeros(vocab_size, device=device, dtype=dtype)