Coverage for transformer_lens/model_bridge/supported_architectures/phi3.py: 81%

119 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-01 16:23 +0000

1"""Phi-3 architecture adapter.""" 

2 

3from typing import Any 

4 

5import torch 

6 

7from transformer_lens.conversion_utils.conversion_steps import ( 

8 RearrangeTensorConversion, 

9 SplitTensorConversion, 

10) 

11from transformer_lens.conversion_utils.conversion_steps.base_tensor_conversion import ( 

12 BaseTensorConversion, 

13) 

14from transformer_lens.conversion_utils.param_processing_conversion import ( 

15 ParamProcessingConversion, 

16) 

17from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

18from transformer_lens.model_bridge.compat import patch_dynamic_cache_v5 

19from transformer_lens.model_bridge.generalized_components import ( 

20 BlockBridge, 

21 EmbeddingBridge, 

22 JointGateUpMLPBridge, 

23 JointQKVPositionEmbeddingsAttentionBridge, 

24 LinearBridge, 

25 RMSNormalizationBridge, 

26 RotaryEmbeddingBridge, 

27 UnembeddingBridge, 

28) 

29from transformer_lens.utilities.quantization import require_readable_weight 

30 

31 

32class _SizedSplitConversion(BaseTensorConversion): 

33 """Split a tensor using explicit sizes (for GQA where Q/K/V have different dimensions).""" 

34 

35 def __init__(self, sizes: list[int], index: int, dim: int = 0): 

36 super().__init__() 

37 self.sizes = sizes 

38 self.index = index 

39 self.dim = dim 

40 

41 def handle_conversion(self, input_value: torch.Tensor, *full_context: Any) -> torch.Tensor: 

42 parts = torch.split(input_value, self.sizes, dim=self.dim) 

43 return parts[self.index] 

44 

45 

46class Phi3ArchitectureAdapter(ArchitectureAdapter): 

47 """Architecture adapter for Phi-3 models.""" 

48 

49 _testing_eager = None 

50 

51 def __init__(self, cfg: Any) -> None: 

52 """Initialize the Phi-3 architecture adapter. 

53 

54 Args: 

55 cfg: The configuration object. 

56 """ 

57 super().__init__(cfg) 

58 

59 self._set_rms_rotary_defaults() 

60 

61 # Standard fold_ln can't handle joint qkv/gate_up projections (shape mismatch). 

62 # LN folding is handled in preprocess_weights() instead. 

63 self.supports_fold_ln = False 

64 

65 # GQA: Q has n_heads * d_head, K/V have n_kv_heads * d_head. 

66 # cfg.d_head honours an explicit HF head_dim, which need not equal 

67 # d_model // n_heads. 

68 d_head = cfg.d_head 

69 n_kv_heads = cfg.n_key_value_heads or cfg.n_heads 

70 q_size = cfg.n_heads * d_head 

71 kv_size = n_kv_heads * d_head 

72 qkv_sizes = [q_size, kv_size, kv_size] 

73 

74 self.weight_processing_conversions = { 

75 "blocks.{i}.attn.q": ParamProcessingConversion( 

76 tensor_conversion=_SizedSplitConversion(qkv_sizes, 0), 

77 source_key="model.layers.{i}.self_attn.qkv_proj.weight", 

78 ), 

79 "blocks.{i}.attn.k": ParamProcessingConversion( 

80 tensor_conversion=_SizedSplitConversion(qkv_sizes, 1), 

81 source_key="model.layers.{i}.self_attn.qkv_proj.weight", 

82 ), 

83 "blocks.{i}.attn.v": ParamProcessingConversion( 

84 tensor_conversion=_SizedSplitConversion(qkv_sizes, 2), 

85 source_key="model.layers.{i}.self_attn.qkv_proj.weight", 

86 ), 

87 "blocks.{i}.attn.o": ParamProcessingConversion( 

88 tensor_conversion=RearrangeTensorConversion("m (n h) -> n h m", n=self.cfg.n_heads), 

89 source_key="model.layers.{i}.self_attn.o_proj.weight", 

90 ), 

91 "blocks.{i}.mlp.in": ParamProcessingConversion( 

92 tensor_conversion=SplitTensorConversion(1, 2), 

93 source_key="model.layers.{i}.mlp.gate_up_proj.weight", 

94 ), 

95 "blocks.{i}.mlp.gate": ParamProcessingConversion( 

96 tensor_conversion=SplitTensorConversion(0, 2), 

97 source_key="model.layers.{i}.mlp.gate_up_proj.weight", 

98 ), 

99 } 

100 

101 # Set up component mapping 

102 self.component_mapping = { 

103 "embed": EmbeddingBridge(name="model.embed_tokens"), 

104 "rotary_emb": RotaryEmbeddingBridge(name="model.rotary_emb"), 

105 "blocks": BlockBridge( 

106 name="model.layers", 

107 submodules={ 

108 "ln1": RMSNormalizationBridge(name="input_layernorm", config=self.cfg), 

109 "ln2": RMSNormalizationBridge(name="post_attention_layernorm", config=self.cfg), 

110 "attn": JointQKVPositionEmbeddingsAttentionBridge( 

111 name="self_attn", 

112 config=self.cfg, 

113 split_qkv_matrix=self._split_phi3_qkv, 

114 submodules={ 

115 "qkv": LinearBridge(name="qkv_proj"), 

116 "o": LinearBridge(name="o_proj"), 

117 }, 

118 ), 

119 "mlp": JointGateUpMLPBridge( 

120 name="mlp", 

121 config=self.cfg, 

122 split_gate_up_matrix=self._split_gate_up, 

123 submodules={ 

124 "out": LinearBridge(name="down_proj"), 

125 }, 

126 ), 

127 }, 

128 ), 

129 "ln_final": RMSNormalizationBridge(name="model.norm", config=self.cfg), 

130 "unembed": UnembeddingBridge(name="lm_head"), 

131 } 

132 

133 @staticmethod 

134 def _split_gate_up( 

135 original_mlp_component: Any, 

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

137 """Split Phi-3's fused gate_up_proj into separate gate and up Linear modules.""" 

138 # This override, not the guarded default, is what Phi-3, GLM and GLM-4V 

139 # install — so the guard has to be here too. FP8 is the case that needs 

140 # it: tensor_split and nn.Parameter both accept it without complaint. 

141 fused_weight = require_readable_weight( 

142 original_mlp_component.gate_up_proj.weight, 

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

144 owner=original_mlp_component.gate_up_proj, 

145 ) 

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

147 d_model = fused_weight.shape[1] 

148 d_mlp = gate_w.shape[0] 

149 

150 has_bias = ( 

151 hasattr(original_mlp_component.gate_up_proj, "bias") 

152 and original_mlp_component.gate_up_proj.bias is not None 

153 ) 

154 gate_b: torch.Tensor | None 

155 up_b: torch.Tensor | None 

156 if has_bias: 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true

157 gate_b, up_b = torch.tensor_split(original_mlp_component.gate_up_proj.bias, 2, dim=0) 

158 else: 

159 gate_b = up_b = None 

160 

161 gate_proj = torch.nn.Linear(d_model, d_mlp, bias=has_bias) 

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

163 if gate_b is not None: 163 ↛ 164line 163 didn't jump to line 164 because the condition on line 163 was never true

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

165 

166 up_proj = torch.nn.Linear(d_model, d_mlp, bias=has_bias) 

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

168 if up_b is not None: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true

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

170 

171 return gate_proj, up_proj 

172 

173 def _split_phi3_qkv( 

174 self, original_attention_component: Any 

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

176 """Split Phi-3's fused qkv_proj into separate Q, K, V linear modules.""" 

177 qkv_weight = require_readable_weight( 

178 original_attention_component.qkv_proj.weight, 

179 operation="split a fused QKV projection at boot", 

180 owner=original_attention_component.qkv_proj, 

181 ) 

182 d_model = qkv_weight.shape[1] 

183 

184 # GQA: Q has n_heads * d_head, K/V have n_kv_heads * d_head each. 

185 # cfg.d_head honours an explicit HF head_dim, which need not equal 

186 # d_model // n_heads. 

187 d_head = self.cfg.d_head 

188 n_kv_heads = self.cfg.n_key_value_heads or self.cfg.n_heads 

189 q_size = self.cfg.n_heads * d_head 

190 kv_size = n_kv_heads * d_head 

191 q_weight, k_weight, v_weight = torch.split(qkv_weight, [q_size, kv_size, kv_size], dim=0) 

192 

193 has_bias = ( 

194 hasattr(original_attention_component.qkv_proj, "bias") 

195 and original_attention_component.qkv_proj.bias is not None 

196 ) 

197 q_bias: torch.Tensor | None 

198 k_bias: torch.Tensor | None 

199 v_bias: torch.Tensor | None 

200 if has_bias: 

201 q_bias, k_bias, v_bias = torch.split( 

202 original_attention_component.qkv_proj.bias, [q_size, kv_size, kv_size], dim=0 

203 ) 

204 else: 

205 q_bias = k_bias = v_bias = None 

206 

207 q_linear = torch.nn.Linear(d_model, q_weight.shape[0], bias=has_bias) 

208 q_linear.weight = torch.nn.Parameter(q_weight) 

209 if q_bias is not None: 

210 q_linear.bias = torch.nn.Parameter(q_bias) 

211 

212 k_linear = torch.nn.Linear(d_model, k_weight.shape[0], bias=has_bias) 

213 k_linear.weight = torch.nn.Parameter(k_weight) 

214 if k_bias is not None: 

215 k_linear.bias = torch.nn.Parameter(k_bias) 

216 

217 v_linear = torch.nn.Linear(d_model, v_weight.shape[0], bias=has_bias) 

218 v_linear.weight = torch.nn.Parameter(v_weight) 

219 if v_bias is not None: 

220 v_linear.bias = torch.nn.Parameter(v_bias) 

221 

222 return q_linear, k_linear, v_linear 

223 

224 def prepare_loading(self, model_name: str, model_kwargs: dict) -> None: 

225 """Patch cached Phi-3 remote code for transformers v5 compatibility.""" 

226 uses_remote_code = model_kwargs.get("trust_remote_code", False) 

227 if not uses_remote_code: 

228 return 

229 

230 config = model_kwargs.get("config") 

231 if config is not None: 

232 rope_scaling = getattr(config, "rope_scaling", None) 

233 if isinstance(rope_scaling, dict) and rope_scaling.get("rope_type") == "default": 

234 config.rope_scaling = None 

235 

236 patch_dynamic_cache_v5() 

237 

238 def preprocess_weights(self, state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: 

239 """Fold layer norms into joint QKV/gate_up projections. 

240 

241 Standard fold_ln can't handle joint projections (shape mismatch on round-trip), 

242 so we scale the full joint weights directly. 

243 """ 

244 fold_ln = getattr(self, "_fold_ln_requested", True) 

245 if not fold_ln: 

246 return state_dict 

247 

248 n_layers = self.cfg.n_layers 

249 

250 for i in range(n_layers): 

251 ln1_key = f"blocks.{i}.ln1.weight" 

252 ln2_key = f"blocks.{i}.ln2.weight" 

253 

254 # Fold ln1 into qkv_proj 

255 if ln1_key in state_dict: 255 ↛ 270line 255 didn't jump to line 270 because the condition on line 255 was always true

256 ln1_w = state_dict[ln1_key].float() 

257 for qkv_key in [ 

258 f"blocks.{i}.attn.q.weight", 

259 f"blocks.{i}.attn.k.weight", 

260 f"blocks.{i}.attn.v.weight", 

261 ]: 

262 if qkv_key in state_dict: 262 ↛ 257line 262 didn't jump to line 257 because the condition on line 262 was always true

263 orig_dtype = state_dict[qkv_key].dtype 

264 state_dict[qkv_key] = (state_dict[qkv_key].float() * ln1_w[None, :]).to( 

265 orig_dtype 

266 ) 

267 state_dict[ln1_key] = torch.ones_like(state_dict[ln1_key]) 

268 

269 # Fold ln2 into gate_up_proj 

270 if ln2_key in state_dict: 270 ↛ 250line 270 didn't jump to line 250 because the condition on line 270 was always true

271 ln2_w = state_dict[ln2_key].float() 

272 for mlp_key in [ 

273 f"blocks.{i}.mlp.gate.weight", 

274 f"blocks.{i}.mlp.in.weight", 

275 ]: 

276 if mlp_key in state_dict: 276 ↛ 272line 276 didn't jump to line 272 because the condition on line 276 was always true

277 orig_dtype = state_dict[mlp_key].dtype 

278 state_dict[mlp_key] = (state_dict[mlp_key].float() * ln2_w[None, :]).to( 

279 orig_dtype 

280 ) 

281 state_dict[ln2_key] = torch.ones_like(state_dict[ln2_key]) 

282 

283 # Fold ln_final into unembed 

284 ln_final_key = "ln_final.weight" 

285 unembed_key = "unembed.weight" 

286 if ln_final_key in state_dict and unembed_key in state_dict: 286 ↛ 296line 286 didn't jump to line 296 because the condition on line 286 was always true

287 ln_final_w = state_dict[ln_final_key].float() 

288 unembed_w = state_dict[unembed_key].float() 

289 orig_dtype = state_dict[unembed_key].dtype 

290 if unembed_w.shape[-1] == ln_final_w.shape[0]: 290 ↛ 292line 290 didn't jump to line 292 because the condition on line 290 was always true

291 state_dict[unembed_key] = (unembed_w * ln_final_w[None, :]).to(orig_dtype) 

292 elif unembed_w.shape[0] == ln_final_w.shape[0]: 

293 state_dict[unembed_key] = (unembed_w * ln_final_w[:, None]).to(orig_dtype) 

294 state_dict[ln_final_key] = torch.ones_like(state_dict[ln_final_key]) 

295 

296 return state_dict