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

85 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-01 16:23 +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.generalized_components.base import ( 

10 GeneralizedComponent, 

11) 

12from transformer_lens.model_bridge.generalized_components.gated_mlp import ( 

13 GatedMLPBridge, 

14 resolve_activation_fn, 

15) 

16from transformer_lens.model_bridge.generalized_components.linear import LinearBridge 

17from transformer_lens.utilities.quantization import require_readable_weight 

18 

19 

20class JointGateUpMLPBridge(GatedMLPBridge): 

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

22 

23 Splits the fused projection into separate LinearBridges and reconstructs 

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

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

26 

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

28 hook_post (before down_proj). 

29 """ 

30 

31 def __init__( 

32 self, 

33 name: str, 

34 config: Optional[Any] = None, 

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

36 split_gate_up_matrix: Optional[Callable] = None, 

37 fused_attr: str = "gate_up_proj", 

38 ): 

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

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

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

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

43 self.fused_attr = fused_attr 

44 self.split_gate_up_matrix = ( 

45 split_gate_up_matrix 

46 if split_gate_up_matrix is not None 

47 else self._default_split_gate_up 

48 ) 

49 

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

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

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

53 _up_bridge = LinearBridge(name="in") 

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

55 

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

57 self.submodules["in"] = _up_bridge 

58 

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

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

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

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

63 

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

65 self._activation_fn: Any = None 

66 

67 self._register_state_dict_hook(JointGateUpMLPBridge._filter_gate_up_state_dict) 

68 self.register_load_state_dict_pre_hook( 

69 JointGateUpMLPBridge._restore_filtered_gate_up_state_dict 

70 ) 

71 

72 @staticmethod 

73 def _filter_gate_up_state_dict( 

74 module: torch.nn.Module, 

75 state_dict: Dict[str, Any], 

76 prefix: str, 

77 local_metadata: Dict[str, Any], 

78 ) -> None: 

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

80 gate_up_prefix = prefix + "gate_up." 

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

82 for k in keys_to_remove: 

83 del state_dict[k] 

84 

85 @staticmethod 

86 def _restore_filtered_gate_up_state_dict( 

87 module: torch.nn.Module, 

88 state_dict: Dict[str, Any], 

89 prefix: str, 

90 local_metadata: Dict[str, Any], 

91 strict: bool, 

92 missing_keys: list[str], 

93 unexpected_keys: list[str], 

94 error_msgs: list[str], 

95 ) -> None: 

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

97 

98 Production checkpoints restore authoritative values through the unfiltered 

99 Hugging Face ``_original_component`` path. 

100 """ 

101 del local_metadata, strict, missing_keys, unexpected_keys, error_msgs 

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

103 if gate_up is None: 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true

104 return 

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

106 state_dict.setdefault(key, value) 

107 

108 def _default_split_gate_up( 

109 self, 

110 original_mlp_component: Any, 

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

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

113 fused_module = getattr(original_mlp_component, self.fused_attr) 

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

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

116 fused_weight = require_readable_weight( 

117 fused_module.weight, 

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

119 owner=fused_module, 

120 ) 

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

122 d_model = fused_weight.shape[1] 

123 d_mlp = gate_w.shape[0] 

124 

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

126 gate_b: torch.Tensor | None = None 

127 up_b: torch.Tensor | None = None 

128 if has_bias: 

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

130 

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

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

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

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

135 if gate_b is not None: 

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

137 

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

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

140 if up_b is not None: 

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

142 

143 return gate_proj, up_proj 

144 

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

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

147 super().set_original_component(original_component) 

148 

149 gate_proj, up_proj = self.split_gate_up_matrix(original_component) 

150 self.gate.set_original_component(gate_proj) 

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

152 

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

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

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

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

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

158 candidate = getattr(original_component, attr, None) 

159 if callable(candidate): 

160 self._activation_fn = candidate 

161 break 

162 

163 def _resolve_activation_fn(self) -> Callable: 

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

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

166 return self._activation_fn 

167 return resolve_activation_fn(self.config) 

168 

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

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

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

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

173 if ( 

174 hasattr(self, "_use_processed_weights") 

175 and self._use_processed_weights 

176 and hasattr(self, "_processed_W_gate") 

177 and hasattr(self, "_processed_W_in") 

178 ): 

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

180 

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

182 

183 gate_output = self.gate(hidden_states) 

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

185 

186 act_fn = self._resolve_activation_fn() 

187 gated = act_fn(gate_output) * up_output 

188 

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 ) 

196 

197 return self.hook_out(output)