Coverage for transformer_lens/model_bridge/supported_architectures/openelm.py: 50%

79 statements  

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

1"""OpenELM architecture adapter.""" 

2 

3import sys 

4from typing import Any 

5 

6import torch 

7 

8from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

9from transformer_lens.model_bridge.compat import patch_dynamic_cache_v5 

10from transformer_lens.model_bridge.generalized_components import ( 

11 BlockBridge, 

12 EmbeddingBridge, 

13 JointGateUpMLPBridge, 

14 LinearBridge, 

15 RMSNormalizationBridge, 

16 UnembeddingBridge, 

17) 

18from transformer_lens.model_bridge.generalized_components.attention import ( 

19 AttentionBridge, 

20) 

21 

22 

23class OpenElmArchitectureAdapter(ArchitectureAdapter): 

24 """Architecture adapter for Apple OpenELM models. 

25 

26 OpenELM uses a unique architecture with per-layer varying head counts and FFN 

27 dimensions. Key characteristics: 

28 

29 - Combined QKV projection (qkv_proj) with per-layer varying Q/KV head counts 

30 - Gated MLP with combined gate+up projection (proj_1) and per-layer FFN sizes 

31 - RMSNorm normalization 

32 - Full rotary embeddings (per-layer, not shared) 

33 - Optional Q/K RMSNorm (normalize_qk_projections=True) 

34 - Weight tying (share_input_output_layers=True typically) 

35 - Model root is 'transformer' (not 'model') 

36 - Requires trust_remote_code=True (custom HF code) 

37 

38 The native HF attention handles all per-layer dimension variations, RoPE, 

39 GQA group repeat, and Q/K normalization internally. The bridge delegates 

40 to the native forward for correct computation. 

41 

42 Note: Individual Q/K/V hooks are not available since the model uses a combined 

43 QKV projection. Attention-level hooks (hook_attn_in, hook_attn_out) are provided. 

44 """ 

45 

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

47 """Initialize the OpenELM architecture adapter.""" 

48 super().__init__(cfg) 

49 

50 self._set_rms_rotary_defaults() 

51 

52 # OpenELM doesn't ship its own tokenizer — uses LLaMA tokenizer. 

53 # Use NousResearch mirror (ungated) to avoid access restrictions. 

54 self.cfg.tokenizer_name = "NousResearch/Llama-2-7b-hf" 

55 

56 # No weight processing conversions needed - native attention handles all 

57 # per-layer dimension variations internally 

58 self.weight_processing_conversions = {} 

59 

60 # Store reference for RoPE patching 

61 self._original_rope_compute = None 

62 self._rope_class = None 

63 

64 self.component_mapping = { 

65 "embed": EmbeddingBridge(name="transformer.token_embeddings"), 

66 "blocks": BlockBridge( 

67 name="transformer.layers", 

68 submodules={ 

69 "ln1": RMSNormalizationBridge(name="attn_norm", config=self.cfg), 

70 "ln2": RMSNormalizationBridge(name="ffn_norm", config=self.cfg), 

71 "attn": AttentionBridge( 

72 name="attn", 

73 config=self.cfg, 

74 submodules={ 

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

76 "o": LinearBridge(name="out_proj"), 

77 }, 

78 maintain_native_attention=True, 

79 requires_attention_mask=True, 

80 # Fused qkv_proj (and per-layer head counts): the q/k/v 

81 # aliases cannot resolve; hook_z stays via out_proj. 

82 fused_qkv=True, 

83 ), 

84 # proj_1 is a FUSED gate+up projection: under a plain 

85 # MLPBridge, hook_pre was the concatenated pre-GLU tensor. 

86 "mlp": JointGateUpMLPBridge( 

87 name="ffn", 

88 config=self.cfg, 

89 fused_attr="proj_1", 

90 submodules={ 

91 "out": LinearBridge(name="proj_2"), 

92 }, 

93 ), 

94 }, 

95 ), 

96 "ln_final": RMSNormalizationBridge(name="transformer.norm", config=self.cfg), 

97 "unembed": UnembeddingBridge(name="lm_head", config=self.cfg), 

98 } 

99 

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

101 """Patch OpenELM for compatibility with transformers v5. 

102 

103 apple's forward also calls DynamicCache.from_legacy_cache / 

104 to_legacy_cache (removed in v5) — restored by the shared 

105 patch_dynamic_cache_v5, as phi3/internlm2/baichuan do. 

106 

107 Two module patches are needed besides that: 

108 1. RotaryEmbedding: Custom _compute_sin_cos_embeddings fails on meta device 

109 because it calls .cos() on meta tensors. We wrap it to catch NotImplementedError. 

110 2. Weight re-initialization: OpenELM's _init_weights re-randomizes ALL weights 

111 after they've been loaded from safetensors because transformers v5's 

112 _finalize_load_state_dict calls initialize_weights() on modules lacking the 

113 _is_hf_initialized flag. We patch _init_weights to skip real (non-meta) tensors. 

114 

115 Args: 

116 model_name: The HuggingFace model name/path 

117 model_kwargs: The kwargs dict for from_pretrained() 

118 """ 

119 patch_dynamic_cache_v5() 

120 

121 # Force-import the modeling module so we can patch it 

122 try: 

123 from transformers.dynamic_module_utils import get_class_from_dynamic_module 

124 

125 get_class_from_dynamic_module( 

126 "modeling_openelm.OpenELMForCausalLM", 

127 model_name, 

128 ) 

129 except Exception: 

130 return 

131 

132 # Find ALL imported OpenELM modules and apply patches. 

133 # Each model variant (e.g., OpenELM-1_1B vs OpenELM-1_1B-Instruct) gets its own 

134 # module in sys.modules with a different cache path, so we patch all of them. 

135 for key in list(sys.modules.keys()): 

136 if "openelm" in key.lower() and "modeling" in key.lower(): 

137 module = sys.modules[key] 

138 if hasattr(module, "OpenELMRotaryEmbedding"): 138 ↛ 163line 138 didn't jump to line 163 because the condition on line 138 was always true

139 rope_class = module.OpenELMRotaryEmbedding 

140 # Skip if already patched (avoid wrapping safe_compute in safe_compute) 

141 if getattr(rope_class, "_tl_patched", False): 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true

142 continue 

143 # Patch 1: RoPE meta device fix 

144 original_compute = rope_class._compute_sin_cos_embeddings 

145 

146 def safe_compute( 

147 self, 

148 key_len, 

149 key_device="cpu", 

150 key_dtype=torch.float32, 

151 _original=original_compute, 

152 ): 

153 try: 

154 _original(self, key_len, key_device, key_dtype) 

155 except NotImplementedError: 

156 pass # Deferred: re-initialized in prepare_model() 

157 

158 rope_class._compute_sin_cos_embeddings = safe_compute 

159 rope_class._tl_patched = True 

160 self._original_rope_compute = original_compute 

161 self._rope_class = rope_class 

162 

163 if hasattr(module, "OpenELMPreTrainedModel"): 163 ↛ 135line 163 didn't jump to line 135 because the condition on line 163 was always true

164 pretrained_class = module.OpenELMPreTrainedModel 

165 if getattr(pretrained_class, "_tl_patched", False): 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true

166 continue 

167 # Patch 2: Prevent _init_weights from re-randomizing loaded weights. 

168 # transformers v5 calls _init_weights on all modules after weight 

169 # materialization. For modules with real (non-meta) tensors, we must 

170 # skip re-initialization to preserve the loaded checkpoint values. 

171 original_init_weights = pretrained_class._init_weights 

172 

173 def safe_init_weights( 

174 self, 

175 mod, 

176 _original=original_init_weights, 

177 ): 

178 # Only initialize modules still on meta device (pre-loading) 

179 first_param = next(mod.parameters(), None) 

180 if first_param is not None and first_param.device.type != "meta": 

181 return # Already loaded from checkpoint — don't re-randomize 

182 _original(self, mod) 

183 

184 pretrained_class._init_weights = safe_init_weights 

185 pretrained_class._tl_patched = True 

186 

187 def prepare_model(self, hf_model: Any) -> None: 

188 """Post-load fixes for non-persistent buffers zeroed during meta materialization. 

189 

190 Transformers v5 creates models on meta device then materializes weights from 

191 checkpoint. Non-persistent buffers (registered with persistent=False) are NOT 

192 in the checkpoint, so they materialize as zeros. OpenELM has two critical 

193 non-persistent buffers that must be recomputed: 

194 

195 1. RoPE inv_freq — zeroed inv_freq produces cos=1, sin=0 for all positions, 

196 destroying positional information entirely. 

197 2. causal_mask — zeroed mask means no causal masking, allowing all positions 

198 to attend to future tokens. Single forward passes appear correct (no future 

199 tokens to leak) but autoregressive generation degenerates immediately. 

200 

201 We also create a synthetic lm_head for weight-tied models. 

202 

203 Note: We intentionally do NOT restore the original _compute_sin_cos_embeddings. 

204 The safe_compute wrapper is functionally equivalent for real (non-meta) tensors, 

205 and keeping it avoids issues when multiple models are loaded in the same process 

206 (e.g., benchmark suite loading both HF reference and bridge models). 

207 

208 Args: 

209 hf_model: The loaded HuggingFace OpenELM model 

210 """ 

211 # ffn_with_glu=False (ungated FFN) would make the fused gate/up split 

212 # halve a projection that is not fused: boot succeeds, then the first 

213 # forward dies in a mat-mul far from the cause. All published OpenELM 

214 # checkpoints use GLU; refuse the config loudly at boot instead. 

215 for module_name, module in hf_model.named_modules(): 215 ↛ 224line 215 didn't jump to line 224 because the loop on line 215 didn't complete

216 if getattr(module, "ffn_with_glu", True) is False: 

217 raise NotImplementedError( 

218 f"OpenELM ffn_with_glu=False on {module_name}: the adapter " 

219 "splits proj_1 as a fused gate+up projection, which an " 

220 "ungated FFN does not have." 

221 ) 

222 # Ensure use_cache is set on config (transformers v5 raises AttributeError 

223 # for missing config attributes, and OpenELM's custom config omits use_cache) 

224 if not hasattr(hf_model.config, "use_cache") or "use_cache" not in hf_model.config.__dict__: 

225 hf_model.config.use_cache = False 

226 

227 # Fix 1: Always recompute causal_mask (non-persistent buffer). 

228 # After meta→real materialization, the buffer may contain garbage values 

229 # (not all zeros) depending on the materializer's memory state. The old 

230 # check `not cm.any()` only recomputed when all zeros, missing cases where 

231 # garbage values are non-zero. Always recompute to guarantee correctness. 

232 if hasattr(hf_model, "transformer") and hasattr(hf_model.transformer, "causal_mask"): 

233 cm = hf_model.transformer.causal_mask 

234 if cm is not None: 

235 seq_len = cm.shape[-1] 

236 correct_mask = torch.triu( 

237 torch.ones(seq_len, seq_len, dtype=cm.dtype, device=cm.device), 

238 diagonal=1, 

239 ) 

240 hf_model.transformer.causal_mask = correct_mask 

241 

242 # Fix 2: Recompute RoPE inv_freq on all layers (non-persistent buffer zeroed 

243 # during materialization), then force-recompute sin/cos embeddings. 

244 if hasattr(hf_model, "transformer") and hasattr(hf_model.transformer, "layers"): 

245 rope_max = getattr(hf_model.config, "rope_max_length", 4096) 

246 for layer in hf_model.transformer.layers: 

247 if hasattr(layer, "attn") and hasattr(layer.attn, "pos_embedding"): 

248 rope = layer.attn.pos_embedding 

249 # Always recompute inv_freq (non-persistent buffer). 

250 # Like causal_mask, inv_freq may contain garbage after meta 

251 # materialization rather than clean zeros. 

252 correct_inv_freq = 1.0 / ( 

253 rope.freq_constant 

254 ** ( 

255 torch.arange(0, rope.model_dim, 2, dtype=torch.float32) / rope.model_dim 

256 ) 

257 ) 

258 rope.inv_freq = correct_inv_freq.to(rope.inv_freq.device) 

259 # Force-recompute sin/cos (may have been computed with zero inv_freq) 

260 rope._cached_cos = None 

261 rope._cached_sin = None 

262 rope._compute_sin_cos_embeddings(rope_max) 

263 

264 # Create synthetic lm_head when embeddings are shared 

265 if getattr(hf_model, "lm_head", None) is None and hasattr(hf_model, "transformer"): 

266 embed = hf_model.transformer.token_embeddings 

267 lm_head = torch.nn.Linear(embed.embedding_dim, embed.num_embeddings, bias=False) 

268 lm_head.weight = embed.weight 

269 hf_model.lm_head = lm_head