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

63 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-10-10 05:10 +0000

1"""OpenELM architecture adapter.""" 

2 

3from typing import Any 

4 

5import torch 

6 

7from transformer_lens.model_bridge.architecture_adapter import ArchitectureAdapter 

8from transformer_lens.model_bridge.compat import patch_dynamic_cache_v5 

9from transformer_lens.model_bridge.generalized_components import ( 

10 BlockBridge, 

11 EmbeddingBridge, 

12 JointGateUpMLPBridge, 

13 LinearBridge, 

14 RMSNormalizationBridge, 

15 UnembeddingBridge, 

16) 

17from transformer_lens.model_bridge.generalized_components.attention import ( 

18 AttentionBridge, 

19) 

20from transformer_lens.model_bridge.supported_architectures._remote_code_compat import ( 

21 force_import_remote_class, 

22 iter_remote_modeling_modules, 

23 patch_init_weights_skip_loaded, 

24) 

25 

26 

27class OpenElmArchitectureAdapter(ArchitectureAdapter): 

28 """Architecture adapter for Apple OpenELM models. 

29 

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

31 dimensions. Key characteristics: 

32 

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

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

35 - RMSNorm normalization 

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

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

38 - Weight tying (share_input_output_layers=True typically) 

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

40 - Requires trust_remote_code=True (custom HF code) 

41 

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

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

44 to the native forward for correct computation. 

45 

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

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

48 """ 

49 

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

51 """Initialize the OpenELM architecture adapter.""" 

52 super().__init__(cfg) 

53 

54 self._set_rms_rotary_defaults() 

55 

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

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

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

59 

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

61 # per-layer dimension variations internally 

62 self.weight_processing_conversions = {} 

63 

64 # Store reference for RoPE patching 

65 self._original_rope_compute = None 

66 self._rope_class = None 

67 

68 self.component_mapping = { 

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

70 "blocks": BlockBridge( 

71 name="transformer.layers", 

72 submodules={ 

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

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

75 "attn": AttentionBridge( 

76 name="attn", 

77 config=self.cfg, 

78 submodules={ 

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

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

81 }, 

82 maintain_native_attention=True, 

83 requires_attention_mask=True, 

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

85 # aliases cannot resolve; hook_z stays via out_proj. 

86 fused_qkv=True, 

87 ), 

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

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

90 "mlp": JointGateUpMLPBridge( 

91 name="ffn", 

92 config=self.cfg, 

93 fused_attr="proj_1", 

94 submodules={ 

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

96 }, 

97 ), 

98 }, 

99 ), 

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

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

102 } 

103 

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

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

106 

107 apple's forward also calls DynamicCache.from_legacy_cache / 

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

109 patch_dynamic_cache_v5, as phi3/internlm2/baichuan do. 

110 

111 Two module patches are needed besides that: 

112 1. RotaryEmbedding: Custom _compute_sin_cos_embeddings fails on meta device 

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

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

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

116 _finalize_load_state_dict calls initialize_weights() on modules lacking the 

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

118 

119 Args: 

120 model_name: The HuggingFace model name/path 

121 model_kwargs: The kwargs dict for from_pretrained() 

122 """ 

123 patch_dynamic_cache_v5() 

124 

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

126 if ( 126 ↛ 134line 126 didn't jump to line 134 because the condition on line 126 was never true

127 force_import_remote_class( 

128 model_name, 

129 "modeling_openelm.OpenELMForCausalLM", 

130 revision=model_kwargs.get("code_revision") or model_kwargs.get("revision"), 

131 ) 

132 is None 

133 ): 

134 return 

135 

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

137 # own module in sys.modules with a different cache path; patch all of them. 

138 for module in iter_remote_modeling_modules("openelm"): 

139 rope_class = getattr(module, "OpenELMRotaryEmbedding", None) 

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

141 if rope_class is not None and not getattr(rope_class, "_tl_patched", False): 141 ↛ 163line 141 didn't jump to line 163 because the condition on line 141 was always true

142 # Patch 1: RoPE meta device fix 

143 original_compute = rope_class._compute_sin_cos_embeddings 

144 

145 def safe_compute( 

146 self, 

147 key_len, 

148 key_device="cpu", 

149 key_dtype=torch.float32, 

150 _original=original_compute, 

151 ): 

152 try: 

153 _original(self, key_len, key_device, key_dtype) 

154 except NotImplementedError: 

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

156 

157 rope_class._compute_sin_cos_embeddings = safe_compute 

158 rope_class._tl_patched = True 

159 self._original_rope_compute = original_compute 

160 self._rope_class = rope_class 

161 

162 # Patch 2: don't let _init_weights re-randomize loaded weights. 

163 pretrained_class = getattr(module, "OpenELMPreTrainedModel", None) 

164 if pretrained_class is not None: 164 ↛ 138line 164 didn't jump to line 138 because the condition on line 164 was always true

165 patch_init_weights_skip_loaded(pretrained_class) 

166 

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

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

169 

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

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

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

173 non-persistent buffers that must be recomputed: 

174 

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

176 destroying positional information entirely. 

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

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

179 tokens to leak) but autoregressive generation degenerates immediately. 

180 

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

182 

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

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

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

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

187 

188 Args: 

189 hf_model: The loaded HuggingFace OpenELM model 

190 """ 

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

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

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

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

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

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

197 raise NotImplementedError( 

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

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

200 "ungated FFN does not have." 

201 ) 

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

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

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

205 hf_model.config.use_cache = False 

206 

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

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

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

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

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

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

213 cm = hf_model.transformer.causal_mask 

214 if cm is not None: 

215 seq_len = cm.shape[-1] 

216 correct_mask = torch.triu( 

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

218 diagonal=1, 

219 ) 

220 hf_model.transformer.causal_mask = correct_mask 

221 

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

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

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

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

226 for layer in hf_model.transformer.layers: 

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

228 rope = layer.attn.pos_embedding 

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

230 # Like causal_mask, inv_freq may contain garbage after meta 

231 # materialization rather than clean zeros. 

232 correct_inv_freq = 1.0 / ( 

233 rope.freq_constant 

234 ** ( 

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

236 ) 

237 ) 

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

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

240 rope._cached_cos = None 

241 rope._cached_sin = None 

242 rope._compute_sin_cos_embeddings(rope_max) 

243 

244 # Create synthetic lm_head when embeddings are shared 

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

246 embed = hf_model.transformer.token_embeddings 

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

248 lm_head.weight = embed.weight 

249 hf_model.lm_head = lm_head