Coverage for transformer_lens/model_bridge/generalized_components/joint_gate_up_mlp.py: 94%
86 statements
« prev ^ index » next coverage.py v7.10.1, created at 2026-10-23 20:45 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2026-10-23 20:45 +0000
1"""Bridge component for MLP layers with fused gate+up projections (e.g., Phi-3)."""
2from __future__ import annotations
4from collections.abc import Callable
5from typing import Any, Dict, Optional
7import torch
9from transformer_lens.model_bridge.generalized_components.base import (
10 GeneralizedComponent,
11 align_offloaded_subtree,
12)
13from transformer_lens.model_bridge.generalized_components.gated_mlp import (
14 GatedMLPBridge,
15 resolve_activation_fn,
16)
17from transformer_lens.model_bridge.generalized_components.linear import LinearBridge
18from transformer_lens.utilities.quantization import require_readable_weight
21class JointGateUpMLPBridge(GatedMLPBridge):
22 """Bridge for MLPs with fused gate+up projections (e.g., Phi-3's gate_up_proj).
24 Splits the fused projection into separate LinearBridges and reconstructs
25 the gated MLP forward pass, allowing individual hook access to gate and up
26 activations. Follows the same pattern as JointQKVAttentionBridge for fused QKV.
28 Hook interface matches GatedMLPBridge: hook_pre (gate), hook_pre_linear (up),
29 hook_post (before down_proj).
30 """
32 def __init__(
33 self,
34 name: str,
35 config: Optional[Any] = None,
36 submodules: Optional[Dict[str, GeneralizedComponent]] = None,
37 split_gate_up_matrix: Optional[Callable] = None,
38 fused_attr: str = "gate_up_proj",
39 ):
40 super().__init__(name, config, submodules=submodules)
41 # HF names the fused module differently per family (gate_up_proj,
42 # input_linear, proj_1); parametrizing the default splitter keeps one
43 # guarded, bias-aware implementation instead of per-adapter copies.
44 self.fused_attr = fused_attr
45 self.split_gate_up_matrix = (
46 split_gate_up_matrix
47 if split_gate_up_matrix is not None
48 else self._default_split_gate_up
49 )
51 # Up projection registered as "in" to match GatedMLPBridge convention
52 # (hook_aliases, property_aliases, and weight keys all use "in").
53 self.gate = LinearBridge(name="gate")
54 _up_bridge = LinearBridge(name="in")
55 setattr(self, "in", _up_bridge) # "in" is a keyword; use setattr
57 self.submodules["gate"] = self.gate
58 self.submodules["in"] = _up_bridge
60 self.real_components["gate"] = ("gate", self.gate)
61 self.real_components["in"] = ("in", _up_bridge)
62 if hasattr(self, "out"): 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 self.real_components["out"] = ("out", self.out)
65 # Typed as Any: HF exposes activation_fn as nn.Module (e.g. nn.SiLU)
66 self._activation_fn: Any = None
68 self._register_state_dict_hook(JointGateUpMLPBridge._filter_gate_up_state_dict)
69 self.register_load_state_dict_pre_hook(
70 JointGateUpMLPBridge._restore_filtered_gate_up_state_dict
71 )
73 @staticmethod
74 def _filter_gate_up_state_dict(
75 module: torch.nn.Module,
76 state_dict: Dict[str, Any],
77 prefix: str,
78 local_metadata: Dict[str, Any],
79 ) -> None:
80 """State dict hook that removes stale combined gate_up entries."""
81 gate_up_prefix = prefix + "gate_up."
82 keys_to_remove = [k for k in state_dict if k.startswith(gate_up_prefix)]
83 for k in keys_to_remove:
84 del state_dict[k]
86 @staticmethod
87 def _restore_filtered_gate_up_state_dict(
88 module: torch.nn.Module,
89 state_dict: Dict[str, Any],
90 prefix: str,
91 local_metadata: Dict[str, Any],
92 strict: bool,
93 missing_keys: list[str],
94 unexpected_keys: list[str],
95 error_msgs: list[str],
96 ) -> None:
97 """Insert current combined weights only to satisfy strict key matching.
99 Production checkpoints restore authoritative values through the unfiltered
100 Hugging Face ``_original_component`` path.
101 """
102 del local_metadata, strict, missing_keys, unexpected_keys, error_msgs
103 gate_up = module._modules.get("gate_up")
104 if gate_up is None:
105 return
106 for key, value in gate_up.state_dict(prefix=f"{prefix}gate_up.").items():
107 state_dict.setdefault(key, value)
109 def _default_split_gate_up(
110 self,
111 original_mlp_component: Any,
112 ) -> tuple[torch.nn.Module, torch.nn.Module]:
113 """Split the fused [2*d_mlp, d_model] projection into (gate, up) Linears."""
114 fused_module = getattr(original_mlp_component, self.fused_attr)
115 # Guard before the split: float8 slices and survives nn.Parameter()
116 # silently, producing scale-less projections with no error anywhere.
117 fused_weight = require_readable_weight(
118 fused_module.weight,
119 operation="split a fused gate/up projection at boot",
120 owner=fused_module,
121 )
122 gate_w, up_w = torch.tensor_split(fused_weight, 2, dim=0)
123 d_model = fused_weight.shape[1]
124 d_mlp = gate_w.shape[0]
126 has_bias = getattr(fused_module, "bias", None) is not None
127 gate_b: torch.Tensor | None = None
128 up_b: torch.Tensor | None = None
129 if has_bias:
130 gate_b, up_b = torch.tensor_split(fused_module.bias, 2, dim=0)
132 # skip_init: these Linears exist only to carry the split views, so a
133 # kaiming init would be waste and would advance the global RNG.
134 gate_proj = torch.nn.utils.skip_init(torch.nn.Linear, d_model, d_mlp, bias=has_bias)
135 gate_proj.weight = torch.nn.Parameter(gate_w)
136 if gate_b is not None:
137 gate_proj.bias = torch.nn.Parameter(gate_b)
139 up_proj = torch.nn.utils.skip_init(torch.nn.Linear, d_model, d_mlp, bias=has_bias)
140 up_proj.weight = torch.nn.Parameter(up_w)
141 if up_b is not None:
142 up_proj.bias = torch.nn.Parameter(up_b)
144 return gate_proj, up_proj
146 def set_original_component(self, original_component: torch.nn.Module) -> None:
147 """Set the original MLP component and split fused projections."""
148 super().set_original_component(original_component)
150 # Same setup-time raw-weight read as JointQKVAttentionBridge.set_original_component:
151 # split_gate_up_matrix reads original_component.gate_up_proj directly, once, to build
152 # independent gate/up slices - needs real (not meta) data materialized for that one read
153 # under Accelerate offload. See align_offloaded_subtree's docstring for why the whole
154 # subtree, not just original_component itself, needs materializing here.
155 with align_offloaded_subtree(original_component):
156 gate_proj, up_proj = self.split_gate_up_matrix(original_component)
157 self.gate.set_original_component(gate_proj)
158 getattr(self, "in").set_original_component(up_proj)
160 # Capture activation function from original component. Attr names vary:
161 # activation_fn/act_fn (phi3, glm), act (OpenELM), activation
162 # (GraniteMoeHybrid shared_mlp). Missing one silently falls back to
163 # cfg.act_fn — which is how OpenELM ran ReLU instead of SiLU.
164 for attr in ("activation_fn", "act_fn", "act", "activation"): 164 ↛ exitline 164 didn't return from function 'set_original_component' because the loop on line 164 didn't complete
165 candidate = getattr(original_component, attr, None)
166 if callable(candidate):
167 self._activation_fn = candidate
168 break
170 def _resolve_activation_fn(self) -> Callable:
171 """Resolve the activation function for the reconstructed forward pass."""
172 if self._activation_fn is not None: 172 ↛ 174line 172 didn't jump to line 174 because the condition on line 172 was always true
173 return self._activation_fn
174 return resolve_activation_fn(self.config)
176 def forward(self, *args: Any, **kwargs: Any) -> torch.Tensor:
177 """Reconstructed gated MLP forward with individual hook access."""
178 if hasattr(self, "_use_processed_weights") and self._use_processed_weights:
179 return super().forward(*args, **kwargs)
181 hidden_states = self.hook_in(args[0])
183 gate_output = self.gate(hidden_states)
184 up_output = getattr(self, "in")(hidden_states)
186 act_fn = self._resolve_activation_fn()
187 gated = act_fn(gate_output) * up_output
189 if hasattr(self, "out") and self.out is not None: 189 ↛ 192line 189 didn't jump to line 192 because the condition on line 189 was always true
190 output = self.out(gated)
191 else:
192 raise RuntimeError(
193 f"No 'out' (down_proj) submodule found in {self.__class__.__name__}. "
194 "Ensure 'out' is provided in submodules."
195 )
197 return self.hook_out(output)