Coverage for transformer_lens/model_bridge/generalized_components/joint_gate_up_mlp.py: 94%

98 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-21 19:27 +0000

1"""Bridge component for MLP layers with fused gate+up projections (e.g., Phi-3).""" 

2from __future__ import annotations 

3 

4from collections.abc import Callable 

5from typing import Any, Dict, Optional 

6 

7import torch 

8 

9from transformer_lens.model_bridge._relevance_rules import half_rule, identity_rule 

10from transformer_lens.model_bridge.generalized_components.base import ( 

11 GeneralizedComponent, 

12 align_offloaded_subtree, 

13) 

14from transformer_lens.model_bridge.generalized_components.gated_mlp import ( 

15 GatedMLPBridge, 

16 identity_rule_supports_activation, 

17 resolve_activation_fn, 

18) 

19from transformer_lens.model_bridge.generalized_components.linear import LinearBridge 

20from transformer_lens.utilities.quantization import require_readable_weight 

21 

22 

23class JointGateUpMLPBridge(GatedMLPBridge): 

24 """Bridge for MLPs with fused gate+up projections (e.g., Phi-3's gate_up_proj). 

25 

26 Splits the fused projection into separate LinearBridges and reconstructs 

27 the gated MLP forward pass, allowing individual hook access to gate and up 

28 activations. Follows the same pattern as JointQKVAttentionBridge for fused QKV. 

29 

30 Hook interface matches GatedMLPBridge: hook_pre (gate), hook_pre_linear (up), 

31 hook_post (before down_proj). 

32 """ 

33 

34 def __init__( 

35 self, 

36 name: str, 

37 config: Optional[Any] = None, 

38 submodules: Optional[Dict[str, GeneralizedComponent]] = None, 

39 split_gate_up_matrix: Optional[Callable] = None, 

40 fused_attr: str = "gate_up_proj", 

41 ): 

42 super().__init__(name, config, submodules=submodules) 

43 # HF names the fused module differently per family (gate_up_proj, 

44 # input_linear, proj_1); parametrizing the default splitter keeps one 

45 # guarded, bias-aware implementation instead of per-adapter copies. 

46 self.fused_attr = fused_attr 

47 self.split_gate_up_matrix = ( 

48 split_gate_up_matrix 

49 if split_gate_up_matrix is not None 

50 else self._default_split_gate_up 

51 ) 

52 

53 # Up projection registered as "in" to match GatedMLPBridge convention 

54 # (hook_aliases, property_aliases, and weight keys all use "in"). 

55 self.gate = LinearBridge(name="gate") 

56 _up_bridge = LinearBridge(name="in") 

57 setattr(self, "in", _up_bridge) # "in" is a keyword; use setattr 

58 

59 self.submodules["gate"] = self.gate 

60 self.submodules["in"] = _up_bridge 

61 

62 self.real_components["gate"] = ("gate", self.gate) 

63 self.real_components["in"] = ("in", _up_bridge) 

64 if hasattr(self, "out"): 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true

65 self.real_components["out"] = ("out", self.out) 

66 

67 # Typed as Any: HF exposes activation_fn as nn.Module (e.g. nn.SiLU) 

68 self._activation_fn: Any = None 

69 

70 self._register_state_dict_hook(JointGateUpMLPBridge._filter_gate_up_state_dict) 

71 self.register_load_state_dict_pre_hook( 

72 JointGateUpMLPBridge._restore_filtered_gate_up_state_dict 

73 ) 

74 

75 @staticmethod 

76 def _filter_gate_up_state_dict( 

77 module: torch.nn.Module, 

78 state_dict: Dict[str, Any], 

79 prefix: str, 

80 local_metadata: Dict[str, Any], 

81 ) -> None: 

82 """State dict hook that removes stale combined gate_up entries.""" 

83 gate_up_prefix = prefix + "gate_up." 

84 keys_to_remove = [k for k in state_dict if k.startswith(gate_up_prefix)] 

85 for k in keys_to_remove: 

86 del state_dict[k] 

87 

88 @staticmethod 

89 def _restore_filtered_gate_up_state_dict( 

90 module: torch.nn.Module, 

91 state_dict: Dict[str, Any], 

92 prefix: str, 

93 local_metadata: Dict[str, Any], 

94 strict: bool, 

95 missing_keys: list[str], 

96 unexpected_keys: list[str], 

97 error_msgs: list[str], 

98 ) -> None: 

99 """Insert current combined weights only to satisfy strict key matching. 

100 

101 Production checkpoints restore authoritative values through the unfiltered 

102 Hugging Face ``_original_component`` path. 

103 """ 

104 del local_metadata, strict, missing_keys, unexpected_keys, error_msgs 

105 gate_up = module._modules.get("gate_up") 

106 if gate_up is None: 

107 return 

108 for key, value in gate_up.state_dict(prefix=f"{prefix}gate_up.").items(): 

109 state_dict.setdefault(key, value) 

110 

111 def _default_split_gate_up( 

112 self, 

113 original_mlp_component: Any, 

114 ) -> tuple[torch.nn.Module, torch.nn.Module]: 

115 """Split the fused [2*d_mlp, d_model] projection into (gate, up) Linears.""" 

116 fused_module = getattr(original_mlp_component, self.fused_attr) 

117 # Guard before the split: float8 slices and survives nn.Parameter() 

118 # silently, producing scale-less projections with no error anywhere. 

119 fused_weight = require_readable_weight( 

120 fused_module.weight, 

121 operation="split a fused gate/up projection at boot", 

122 owner=fused_module, 

123 ) 

124 gate_w, up_w = torch.tensor_split(fused_weight, 2, dim=0) 

125 d_model = fused_weight.shape[1] 

126 d_mlp = gate_w.shape[0] 

127 

128 has_bias = getattr(fused_module, "bias", None) is not None 

129 gate_b: torch.Tensor | None = None 

130 up_b: torch.Tensor | None = None 

131 if has_bias: 

132 gate_b, up_b = torch.tensor_split(fused_module.bias, 2, dim=0) 

133 

134 # skip_init: these Linears exist only to carry the split views, so a 

135 # kaiming init would be waste and would advance the global RNG. 

136 gate_proj = torch.nn.utils.skip_init(torch.nn.Linear, d_model, d_mlp, bias=has_bias) 

137 gate_proj.weight = torch.nn.Parameter(gate_w) 

138 if gate_b is not None: 

139 gate_proj.bias = torch.nn.Parameter(gate_b) 

140 

141 up_proj = torch.nn.utils.skip_init(torch.nn.Linear, d_model, d_mlp, bias=has_bias) 

142 up_proj.weight = torch.nn.Parameter(up_w) 

143 if up_b is not None: 

144 up_proj.bias = torch.nn.Parameter(up_b) 

145 

146 return gate_proj, up_proj 

147 

148 def set_original_component(self, original_component: torch.nn.Module) -> None: 

149 """Set the original MLP component and split fused projections.""" 

150 super().set_original_component(original_component) 

151 

152 # Same setup-time raw-weight read as JointQKVAttentionBridge.set_original_component: 

153 # split_gate_up_matrix reads original_component.gate_up_proj directly, once, to build 

154 # independent gate/up slices - needs real (not meta) data materialized for that one read 

155 # under Accelerate offload. See align_offloaded_subtree's docstring for why the whole 

156 # subtree, not just original_component itself, needs materializing here. 

157 with align_offloaded_subtree(original_component): 

158 gate_proj, up_proj = self.split_gate_up_matrix(original_component) 

159 self.gate.set_original_component(gate_proj) 

160 getattr(self, "in").set_original_component(up_proj) 

161 

162 # Capture activation function from original component. Attr names vary: 

163 # activation_fn/act_fn (phi3, glm), act (OpenELM), activation 

164 # (GraniteMoeHybrid shared_mlp). Missing one silently falls back to 

165 # cfg.act_fn — which is how OpenELM ran ReLU instead of SiLU. 

166 for attr in ("activation_fn", "act_fn", "act", "activation"): 166 ↛ exitline 166 didn't return from function 'set_original_component' because the loop on line 166 didn't complete

167 candidate = getattr(original_component, attr, None) 

168 if callable(candidate): 

169 self._activation_fn = candidate 

170 break 

171 

172 def _resolve_activation_fn(self) -> Callable: 

173 """Resolve the activation function for the reconstructed forward pass.""" 

174 if self._activation_fn is not None: 174 ↛ 176line 174 didn't jump to line 176 because the condition on line 174 was always true

175 return self._activation_fn 

176 return resolve_activation_fn(self.config) 

177 

178 def _activation_rule_installable(self) -> bool: 

179 """The reconstructed forward calls the activation itself, so only the config 

180 activation form gates the Identity-rule; the opaque-path requirement of a 

181 wrappable activation callable does not apply here.""" 

182 return identity_rule_supports_activation(self.config) 

183 

184 # The reconstructed forward applies both rules inline off the boolean flags set 

185 # by the base ``_enable_relevance_rule``/``_disable_relevance_rule``. The 

186 # opaque-path installers must stay disabled: the gate hook lives on the shared 

187 # down projection this forward also calls, so leaving it active would halve the 

188 # gate*up gradient a second time on top of the inline ``half_rule``. 

189 def _install_activation_rule(self) -> None: 

190 return None 

191 

192 def _teardown_activation_rule(self) -> None: 

193 return None 

194 

195 def _install_gate_rule(self) -> None: 

196 return None 

197 

198 def _teardown_gate_rule(self) -> None: 

199 return None 

200 

201 def forward(self, *args: Any, **kwargs: Any) -> torch.Tensor: 

202 """Reconstructed gated MLP forward with individual hook access.""" 

203 # Delegate to GatedMLPBridge's processed-weights path only when ALL 

204 # processed weights exist; its fallback bypasses intermediate hooks. 

205 if ( 

206 hasattr(self, "_use_processed_weights") 

207 and self._use_processed_weights 

208 and hasattr(self, "_processed_W_gate") 

209 and hasattr(self, "_processed_W_in") 

210 ): 

211 return super().forward(*args, **kwargs) 

212 

213 hidden_states = self.hook_in(args[0]) 

214 

215 gate_output = self.gate(hidden_states) 

216 up_output = getattr(self, "in")(hidden_states) 

217 

218 act_fn = self._resolve_activation_fn() 

219 activated = ( 

220 identity_rule(gate_output, act_fn) 

221 if self._relevance_rule_activation_active 

222 else act_fn(gate_output) 

223 ) 

224 gated = ( 

225 half_rule(activated, up_output) 

226 if self._relevance_rule_gate_active 

227 else activated * up_output 

228 ) 

229 

230 if hasattr(self, "out") and self.out is not None: 230 ↛ 233line 230 didn't jump to line 233 because the condition on line 230 was always true

231 output = self.out(gated) 

232 else: 

233 raise RuntimeError( 

234 f"No 'out' (down_proj) submodule found in {self.__class__.__name__}. " 

235 "Ensure 'out' is provided in submodules." 

236 ) 

237 

238 return self.hook_out(output)