Coverage for transformer_lens/model_bridge/generalized_components/position_embeddings_attention.py: 83%

320 statements  

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

1"""Position embeddings attention bridge with full hook support. 

2 

3Reimplements attention for models using RoPE (Llama, Gemma, Qwen, OLMo, etc.) 

4so that all hook points fire at the correct computation stage: 

5- hook_q/hook_k/hook_v: after projection 

6- hook_rot_q/hook_rot_k: after RoPE rotation 

7- hook_attn_scores: PRE-softmax (matching HookedTransformer convention) 

8- hook_pattern: POST-softmax 

9""" 

10from __future__ import annotations 

11 

12import weakref 

13from typing import Any, Callable, Dict, Optional 

14 

15import torch 

16import transformers.models.gemma2.modeling_gemma2 as gemma2_module 

17 

18from transformer_lens.hook_points import HookPoint 

19from transformer_lens.model_bridge.generalized_components.attention import ( 

20 AttentionBridge, 

21) 

22from transformer_lens.model_bridge.generalized_components.position_embedding_hooks_mixin import ( 

23 PositionEmbeddingHooksMixin, 

24) 

25from transformer_lens.utilities.attention import clamp_qkv 

26from transformer_lens.utilities.heterogeneous_config import safe_config_get 

27from transformer_lens.utilities.hf_utils import get_rotary_pct_from_config 

28 

29# Global registry mapping HF attention modules to their bridge instances 

30# Uses WeakValueDictionary to avoid preventing garbage collection of bridges 

31_ATTENTION_BRIDGE_REGISTRY: weakref.WeakValueDictionary = weakref.WeakValueDictionary() 

32 

33# Track whether we've already wrapped eager_attention_forward 

34_EAGER_ATTENTION_WRAPPED = False 

35 

36# Store the original function for restoration 

37_ORIGINAL_EAGER_ATTENTION_FORWARD: Optional[Callable] = None 

38 

39 

40def _apply_rotary_pos_emb_adjacent_pairs( 

41 q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor 

42) -> tuple[torch.Tensor, torch.Tensor]: 

43 """GLM/ERNIE-style RoPE: rotate adjacent element pairs in full precision. 

44 

45 cos/sin arrive in the standard half-duplicated layout; this convention 

46 takes the first half and expands it by repeat_interleave(2) so rotation 

47 pairs are (0,1), (2,3), ... instead of (i, i + d/2). 

48 """ 

49 original_dtype = q.dtype 

50 cos = cos.unsqueeze(1) 

51 sin = sin.unsqueeze(1) 

52 cos = cos[..., : cos.shape[-1] // 2].repeat_interleave(2, dim=-1) 

53 sin = sin[..., : sin.shape[-1] // 2].repeat_interleave(2, dim=-1) 

54 

55 def _rotate(x: torch.Tensor) -> torch.Tensor: 

56 x1 = x[..., 0::2] 

57 x2 = x[..., 1::2] 

58 return torch.stack((-x2, x1), dim=-1).flatten(-2) 

59 

60 q_embed = (q.float() * cos) + (_rotate(q).float() * sin) 

61 k_embed = (k.float() * cos) + (_rotate(k).float() * sin) 

62 return q_embed.to(original_dtype), k_embed.to(original_dtype) 

63 

64 

65def _setup_eager_attention_hook_wrapper() -> None: 

66 """Wrap gemma2's eager_attention_forward to fire hook_rot_q and hook_rot_k. 

67 

68 This function monkey-patches the module-level eager_attention_forward function 

69 to intercept query and key tensors (which have already had rotary embeddings applied) 

70 and fire the corresponding hooks on the registered bridge instance. 

71 

72 This is safe to call multiple times - it will only wrap once. 

73 """ 

74 global _EAGER_ATTENTION_WRAPPED, _ORIGINAL_EAGER_ATTENTION_FORWARD 

75 

76 if _EAGER_ATTENTION_WRAPPED: 

77 return 

78 

79 _ORIGINAL_EAGER_ATTENTION_FORWARD = gemma2_module.eager_attention_forward 

80 

81 def hooked_eager_attention_forward( 

82 module: torch.nn.Module, 

83 query: torch.Tensor, 

84 key: torch.Tensor, 

85 value: torch.Tensor, 

86 attention_mask: Optional[torch.Tensor], 

87 **kwargs: Any, 

88 ) -> tuple: 

89 """Wrapped eager_attention_forward that fires rotary hooks. 

90 

91 Args: 

92 module: The HF attention module (used to look up the bridge) 

93 query: Query tensor AFTER rotary embeddings applied 

94 key: Key tensor AFTER rotary embeddings applied 

95 value: Value tensor 

96 attention_mask: Attention mask 

97 **kwargs: Additional arguments (dropout, scaling, etc.) 

98 

99 Returns: 

100 Tuple of (attn_output, attn_weights) 

101 """ 

102 # Look up the bridge instance for this attention module 

103 bridge = _ATTENTION_BRIDGE_REGISTRY.get(id(module)) 

104 

105 if bridge is not None: 105 ↛ 107line 105 didn't jump to line 107 because the condition on line 105 was never true

106 # Fire hook_rot_q and hook_rot_k with the post-rotary Q/K 

107 if hasattr(bridge, "hook_rot_q"): 

108 query = bridge.hook_rot_q(query) 

109 if hasattr(bridge, "hook_rot_k"): 

110 key = bridge.hook_rot_k(key) 

111 

112 assert _ORIGINAL_EAGER_ATTENTION_FORWARD is not None 

113 return _ORIGINAL_EAGER_ATTENTION_FORWARD( 

114 module, query, key, value, attention_mask, **kwargs 

115 ) 

116 

117 # Replace the module-level function for both Gemma 2 and Gemma 3 

118 gemma2_module.eager_attention_forward = hooked_eager_attention_forward # type: ignore[assignment] 

119 

120 try: 

121 import transformers.models.gemma3.modeling_gemma3 as gemma3_module 

122 

123 gemma3_module.eager_attention_forward = hooked_eager_attention_forward # type: ignore[assignment] 

124 except ImportError: 

125 pass # Gemma 3 not available in this transformers version 

126 

127 _EAGER_ATTENTION_WRAPPED = True 

128 

129 

130class PositionEmbeddingsAttentionBridge(PositionEmbeddingHooksMixin, AttentionBridge): 

131 """Attention bridge for models that require position embeddings (e.g., Gemma-3). 

132 

133 Some models use specialized position embedding systems (like Gemma-3's dual RoPE) 

134 which require position_embeddings to be generated in a specific format that differs 

135 from standard RoPE models. 

136 

137 The position_embeddings are generated by calling the model's rotary_emb 

138 component with dummy Q/K tensors and position_ids. 

139 """ 

140 

141 supports_attn_result: bool = True 

142 

143 # NoPE architectures (EXAONE-4, SmolLM3, Cohere2) deliberately null 

144 # position_embeddings on their non-rotary layers to match HF. Their bridge 

145 # subclasses set this so the missing-RoPE warning stays meaningful. 

146 rope_optional: bool = False 

147 

148 def __init__( 

149 self, 

150 name: str, 

151 config: Any, 

152 submodules: Optional[Dict[str, Any]] = None, 

153 optional: bool = False, 

154 # Accepted for caller compatibility (Granite passes these explicitly) 

155 # but always forced to True — this bridge reimplements attention. 

156 requires_attention_mask: bool = True, 

157 requires_position_embeddings: bool = True, 

158 is_causal: bool = True, 

159 **kwargs, # absorb any other AttentionBridge kwargs callers may pass 

160 ): 

161 super().__init__( 

162 name, 

163 config, 

164 submodules, 

165 requires_position_embeddings=True, 

166 requires_attention_mask=True, 

167 maintain_native_attention=True, 

168 is_causal=is_causal, 

169 optional=optional, 

170 ) 

171 self._init_position_embedding_hooks() 

172 if getattr(config, "gated_q_proj", False): 

173 self.hook_q_gate = HookPoint() 

174 # Gate on adapter intent; HF-vs-adapter mismatches surface in set_original_component. 

175 if submodules is not None and "gate" in submodules: 

176 self.hook_gate = HookPoint() 

177 if submodules is not None and "q_norm" in submodules: 

178 self.hook_q_normed = HookPoint() 

179 if submodules is not None and "k_norm" in submodules: 

180 self.hook_k_normed = HookPoint() 

181 self._qk_norm_phase: Optional[str] = None 

182 

183 def set_original_component(self, component: torch.nn.Module) -> None: 

184 """Wire HF module, register for rotary hooks, validate adapter declarations.""" 

185 super().set_original_component(component) 

186 _ATTENTION_BRIDGE_REGISTRY[id(component)] = self 

187 _setup_eager_attention_hook_wrapper() 

188 self._validate_submodule_declarations(component) 

189 self._qk_norm_phase = self._decide_qk_norm_phase(component) 

190 self._own_scaled_hook_k(component) 

191 

192 def _own_scaled_hook_k(self, hf_attn: torch.nn.Module) -> None: 

193 """Replace the ``hook_k`` alias with a real HookPoint when K is scaled. 

194 

195 Aliased hook_k reported Falcon-H1's pre-key_multiplier tensor and 

196 silently rescaled writes; the owned hook carries the scaled value while 

197 ``k.hook_out`` stays raw (Granite's split). No-op elsewhere. 

198 """ 

199 if getattr(hf_attn, "key_multiplier", None) is None: 

200 return 

201 if self.hook_aliases is type(self).hook_aliases: 201 ↛ 203line 201 didn't jump to line 203 because the condition on line 201 was always true

202 self.hook_aliases = dict(self.hook_aliases) 

203 self.hook_aliases.pop("hook_k", None) 

204 # The per-head hook_conversion is attached later, by 

205 # _setup_qkv_hook_reshaping — component binding always precedes hook 

206 # compatibility setup (bridge.py wires components, then calls it). 

207 self.hook_k = HookPoint() 

208 

209 def _fire_scaled_hook_k(self, key_states: torch.Tensor) -> torch.Tensor: 

210 """Fire an owned ``hook_k`` on the flat 3D tensor, preserving input rank. 

211 

212 A 4D input must flatten first: the conversion's revert only fires on 4D 

213 returns, so an edited tensor would reach RoPE with the wrong rank. 

214 """ 

215 if "hook_k" in self.hook_aliases or not hasattr(self, "hook_k"): 215 ↛ 216line 215 didn't jump to line 216 because the condition on line 215 was never true

216 return key_states 

217 if key_states.dim() == 4: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 b, s, n_h, d_h = key_states.shape 

219 flat = self.hook_k(key_states.reshape(b, s, n_h * d_h)) 

220 return flat.reshape(b, s, n_h, d_h) 

221 return self.hook_k(key_states) 

222 

223 def _validate_submodule_declarations(self, hf_attn: torch.nn.Module) -> None: 

224 """Raise if adapter omits q/k/v/o or a QK-norm the HF module has.""" 

225 # Silent fallback to raw HF linears is exactly what caused hook_q/k/v/z 

226 # to never fire on 25 adapters; require explicit declaration. 

227 missing = [req for req in ("q", "k", "v", "o") if req not in self.submodules] 

228 if missing: 228 ↛ 229line 228 didn't jump to line 229 because the condition on line 228 was never true

229 raise RuntimeError( 

230 f"{type(self).__name__} at '{self.name}' is missing required " 

231 f"submodules: {missing}. Declare them in the adapter's " 

232 f"component_mapping, e.g. submodules={{'q': LinearBridge(name='q_proj'), " 

233 f"'k': LinearBridge(name='k_proj'), 'v': LinearBridge(name='v_proj'), " 

234 f"'o': LinearBridge(name='o_proj')}}." 

235 ) 

236 # Reverse mismatch (adapter declares, HF lacks) surfaces at norm forward. 

237 # HF spells the per-head variants q_layernorm/k_layernorm (StableLM). 

238 for declared, hf_names in ( 

239 ("q_norm", ("q_norm", "q_layernorm")), 

240 ("k_norm", ("k_norm", "k_layernorm")), 

241 ): 

242 hf_present = next((n for n in hf_names if getattr(hf_attn, n, None) is not None), None) 

243 if hf_present is not None and declared not in self.submodules: 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true

244 raise RuntimeError( 

245 f"{type(self).__name__} at '{self.name}': HF module has " 

246 f"'{hf_present}' but adapter did not declare '{declared}'. " 

247 f"Forward would skip the norm, producing wrong logits vs HF. " 

248 f"Add '{declared}' (name='{hf_present}') to the attention " 

249 f"submodules." 

250 ) 

251 

252 def _decide_qk_norm_phase(self, hf_attn: torch.nn.Module) -> Optional[str]: 

253 """Dispatch pre/post-reshape norm from weight shape; raise on ambiguity.""" 

254 if "q_norm" not in self.submodules: 

255 return None 

256 

257 hf_norm_name = self.submodules["q_norm"].name 

258 

259 if hf_norm_name is None: 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true

260 raise RuntimeError(f"{self.name}: q_norm submodule declared without a name.") 

261 

262 q_norm = getattr(hf_attn, hf_norm_name, None) 

263 

264 if q_norm is None: 

265 # Config-gated norms (use_qk_norm, qk_layernorm) declare optional; 

266 # setup pops them after this runs. 

267 if getattr(self.submodules["q_norm"], "optional", False): 

268 return None 

269 raise RuntimeError(f"{self.name}: q_norm declared but HF module has none.") 

270 

271 weight = getattr(q_norm, "weight", None) 

272 head_dim = int(getattr(hf_attn, "head_dim")) 

273 n_heads = int(getattr(self.config, "n_heads", 0)) 

274 

275 # Non-learnable norm (Gemma-3 style) broadcasts over head_dim. 

276 if weight is None or weight.ndim == 0: 

277 return "post_reshape" 

278 shape = tuple(weight.shape) 

279 if shape == (head_dim,): 

280 return "post_reshape" 

281 if n_heads and shape == (n_heads * head_dim,): 281 ↛ 284line 281 didn't jump to line 284 because the condition on line 281 was always true

282 return "pre_reshape" 

283 # Per-head norm (Cohere) broadcasts on the reshaped [B,H,S,D] tensor. 

284 if n_heads and shape == (n_heads, head_dim): 

285 return "post_reshape" 

286 raise RuntimeError( 

287 f"{self.name}: cannot determine QK-norm phase from q_norm weight " 

288 f"shape {shape} (head_dim={head_dim}, n_heads={n_heads}). Expected " 

289 f"(head_dim,), (n_heads*head_dim,), or (n_heads, head_dim)." 

290 ) 

291 

292 @staticmethod 

293 def _apply_pre_reshape_qk_norm( 

294 tensor: torch.Tensor, 

295 norm_module: Any, 

296 hook: Any, 

297 head_dim: int, 

298 ) -> torch.Tensor: 

299 """Apply an OLMo-2-style pre-reshape QK norm, shape-preserving. 

300 

301 The norm computes RMS over the flattened (n_heads * d_head) dim. When 

302 the split path hands us a 4D [B, S, H, d_head], flatten, norm, and 

303 re-split so the result matches what the default 3D path produces at 

304 this point. 

305 """ 

306 if tensor.ndim == 4: 

307 b, s, h, d = tensor.shape 

308 flat = tensor.reshape(b, s, h * d) 

309 normed = hook(norm_module(flat)) 

310 return normed.view(b, s, h, d) 

311 return hook(norm_module(tensor)) 

312 

313 def forward(self, *args: Any, **kwargs: Any) -> Any: 

314 """Reimplemented forward pass with hooks at correct computation stages. 

315 

316 Instead of delegating to the HF attention module (which returns post-softmax 

317 weights), this reimplements attention step-by-step so that: 

318 - hook_attn_scores fires on PRE-softmax scores (matching HookedTransformer) 

319 - hook_pattern fires on POST-softmax weights 

320 - hook_rot_q/hook_rot_k fire after RoPE application 

321 

322 Handles RoPE, GQA, Q/K norms, sliding window, and softcapping. 

323 """ 

324 if self.original_component is None: 324 ↛ 325line 324 didn't jump to line 325 because the condition on line 324 was never true

325 raise RuntimeError( 

326 f"Original component not set for {self.name}. " 

327 "Call set_original_component() first." 

328 ) 

329 

330 # Type as Any — the HF attention module's interface (q_proj, k_proj, etc.) 

331 # varies by architecture and isn't captured by nn.Module's type signature. 

332 hf_attn: Any = self.original_component 

333 

334 # Extract hidden_states and kwargs 

335 if "hidden_states" in kwargs: 

336 hidden_states = kwargs.pop("hidden_states") 

337 elif len(args) > 0 and isinstance(args[0], torch.Tensor): 337 ↛ 341line 337 didn't jump to line 341 because the condition on line 337 was always true

338 hidden_states = args[0] 

339 args = args[1:] 

340 else: 

341 raise ValueError("Could not find hidden_states in args or kwargs") 

342 

343 position_embeddings = kwargs.pop("position_embeddings", None) 

344 attention_mask = kwargs.pop("attention_mask", None) 

345 

346 hidden_states = self.hook_in(hidden_states) 

347 

348 input_shape = hidden_states.shape[:-1] 

349 head_dim = hf_attn.head_dim 

350 hidden_shape = (*input_shape, -1, head_dim) 

351 

352 use_split_qkv = bool(getattr(self.config, "use_split_qkv_input", False)) 

353 use_attn_in = bool(getattr(self.config, "use_attn_in", False)) 

354 has_head_count = ( 

355 self.config is not None and hasattr(self.config, "n_heads") and self.config.n_heads 

356 ) 

357 split_active = (use_split_qkv or use_attn_in) and has_head_count 

358 

359 # Qwen3.5/Qwen3-Next interleave [Q|gate] per head in q_proj output. 

360 # The 2×-width output breaks per-head W slicing, so the split path is 

361 # not supported for gated q_proj. Raise explicitly rather than 

362 # producing silently wrong logits. 

363 if split_active and getattr(self.config, "gated_q_proj", False): 

364 raise NotImplementedError( 

365 "use_split_qkv_input / use_attn_in are not supported on gated " 

366 "q_proj architectures (Qwen3.5 / Qwen3-Next). The 2×-width " 

367 "q_proj output breaks per-head weight routing. If you need " 

368 "this combination, file a bug describing the workflow." 

369 ) 

370 

371 if split_active: 

372 assert self.config is not None # narrowed by `has_head_count` 

373 n_heads = int(self.config.n_heads) 

374 n_kv_heads = int(getattr(self.config, "n_key_value_heads", None) or n_heads) 

375 # #1317: fork pre-LN when available so hook patches match legacy. 

376 captured = self._captured_pre_ln_residual 

377 source = captured if captured is not None else hidden_states 

378 if use_split_qkv: 

379 q_in = self._fork_and_norm_per_head(source, self.hook_q_input, n_heads) 

380 k_in = self._fork_and_norm_per_head(source, self.hook_k_input, n_kv_heads) 

381 v_in = self._fork_and_norm_per_head(source, self.hook_v_input, n_kv_heads) 

382 else: 

383 attn_in = self._fork_and_norm_per_head(source, self.hook_attn_in, n_heads) 

384 q_in = attn_in 

385 if n_kv_heads != n_heads: 

386 k_in = attn_in[..., :n_kv_heads, :].contiguous() 

387 v_in = attn_in[..., :n_kv_heads, :].contiguous() 

388 else: 

389 k_in = v_in = attn_in 

390 query_states = self._project_per_head_qkv(self.q, q_in, n_heads, head_dim) 

391 key_states = self._project_per_head_qkv(self.k, k_in, n_kv_heads, head_dim) 

392 value_states = self._project_per_head_qkv(self.v, v_in, n_kv_heads, head_dim) 

393 q_gate = None 

394 else: 

395 # Route through LinearBridges so hook_q/k/v/z (aliased to 

396 # q/k/v.hook_out, o.hook_in) fire on the live path. 

397 query_states = self.q(hidden_states) 

398 key_states = self.k(hidden_states) 

399 value_states = self.v(hidden_states) 

400 

401 # Qwen3.5/Qwen3-Next interleave [Q|gate] per head in q_proj output. 

402 # Processed-weights mode slices q_proj to standard width beforehand, 

403 # so the 2×-width path only triggers on unprocessed state dicts. 

404 q_gate = None 

405 if getattr(self.config, "gated_q_proj", False): 

406 q_dim = query_states.shape[-1] 

407 n_heads_gated = getattr(self.config, "n_heads", q_dim // head_dim) 

408 standard_q_dim = n_heads_gated * head_dim 

409 if q_dim == standard_q_dim * 2: 

410 query_states, q_gate = torch.chunk( 

411 query_states.view(*input_shape, -1, head_dim * 2), 2, dim=-1 

412 ) 

413 q_gate = q_gate.reshape(*input_shape, -1) 

414 query_states = query_states.reshape(*input_shape, -1) 

415 

416 # Falcon-H1 scales K by a learned mup scalar between projection and RoPE. 

417 # hook_k fires after the scale (see _own_scaled_hook_k) so it carries the 

418 # tensor that actually reaches attention; k.hook_out kept the raw one. 

419 key_multiplier = getattr(hf_attn, "key_multiplier", None) 

420 if key_multiplier is not None: 

421 key_states = key_states * key_multiplier 

422 key_states = self._fire_scaled_hook_k(key_states) 

423 

424 has_q_norm = "q_norm" in self.submodules 

425 has_k_norm = "k_norm" in self.submodules 

426 

427 # Pre-reshape phase (OLMo-2): norm is RMS over the flattened H*d_head 

428 # dim. When the split path produced 4D [B, S, H, d_head], flatten for 

429 # the norm then re-split so the post-norm tensors share shape with the 

430 # non-split path going into the transpose below. 

431 if has_q_norm and self._qk_norm_phase == "pre_reshape": 

432 query_states = self._apply_pre_reshape_qk_norm( 

433 query_states, self.q_norm, self.hook_q_normed, head_dim 

434 ) 

435 if has_k_norm: 435 ↛ 443line 435 didn't jump to line 443 because the condition on line 435 was always true

436 key_states = self._apply_pre_reshape_qk_norm( 

437 key_states, self.k_norm, self.hook_k_normed, head_dim 

438 ) 

439 

440 # OLMo v1 / OLMoE clamp Q/K/V when clip_qkv is set — after the 

441 # pre-reshape qk-norm (OLMoE norms first) and before RoPE, matching HF 

442 # order. HF gates on `is not None` for these archs. 

443 clip_qkv = getattr(getattr(hf_attn, "config", None), "clip_qkv", None) 

444 if clip_qkv is not None: 

445 if self._qk_norm_phase == "post_reshape": 445 ↛ 448line 445 didn't jump to line 448 because the condition on line 445 was never true

446 # No arch pairs clip_qkv with a post-reshape norm; the HF 

447 # ordering is unknowable here, so refuse rather than guess. 

448 raise NotImplementedError( 

449 "clip_qkv with a post-reshape qk-norm has no reference " 

450 "ordering; add the architecture's HF order before enabling." 

451 ) 

452 query_states, key_states, value_states = clamp_qkv( 

453 query_states, key_states, value_states, clip_qkv 

454 ) 

455 

456 # For the split path, tensors are already [B, S, H, d_head]; for the 

457 # default path they're flat [B, S, H*d_head] and need the view. 

458 if split_active: 

459 query_states = query_states.transpose(1, 2) 

460 key_states = key_states.transpose(1, 2) 

461 value_states = value_states.transpose(1, 2) 

462 else: 

463 query_states = query_states.view(hidden_shape).transpose(1, 2) 

464 key_states = key_states.view(hidden_shape).transpose(1, 2) 

465 value_states = value_states.view(hidden_shape).transpose(1, 2) 

466 

467 # Post-reshape phase (Gemma-3/Cohere): norm on [B, H, S, D]. 

468 if has_q_norm and self._qk_norm_phase == "post_reshape": 

469 query_states = self.hook_q_normed(self.q_norm(query_states)) 

470 if has_k_norm: 470 ↛ 474line 470 didn't jump to line 474 because the condition on line 470 was always true

471 key_states = self.hook_k_normed(self.k_norm(key_states)) 

472 

473 # --- RoPE --- 

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

475 position_embeddings is None 

476 and not self.rope_optional 

477 and getattr(self.config, "positional_embedding_type", None) == "rotary" 

478 ): 

479 # Silent skipping is how internlm2 ran without positional encoding 

480 # while reporting perfect parity: its per-layer rotary means the HF 

481 # decoder layer passes nothing down. Adapters in that shape must 

482 # supply position_embeddings themselves (see internlm2.py). 

483 import warnings 

484 

485 warnings.warn( 

486 f"{type(self).__name__}({self.name}) reconstructed attention without " 

487 "position_embeddings on a rotary architecture — RoPE was NOT applied. " 

488 "The adapter must supply them (e.g. from the layer's own rotary_emb).", 

489 RuntimeWarning, 

490 stacklevel=2, 

491 ) 

492 if position_embeddings is not None: 

493 position_embeddings = self._apply_position_embedding_hooks(position_embeddings) 

494 cos, sin = position_embeddings 

495 if getattr(self.config, "rotary_adjacent_pairs", False): 

496 # GLM/ERNIE convention: rotate adjacent element pairs in fp32, 

497 # with cos/sin halves expanded by repeat_interleave. 

498 apply_rotary_pos_emb = _apply_rotary_pos_emb_adjacent_pairs 

499 else: 

500 from transformers.models.llama.modeling_llama import ( 

501 apply_rotary_pos_emb, 

502 ) 

503 

504 rotary_dim = cos.shape[-1] 

505 if rotary_dim * 2 == head_dim and get_rotary_pct_from_config(self.config) == 1.0: 

506 # GPT-OSS convention: full rotation with un-duplicated half-width 

507 # cos/sin. Duplicating the halves reduces it to llama's 

508 # rotate-half formula over the full head_dim. 

509 cos = torch.cat([cos, cos], dim=-1) 

510 sin = torch.cat([sin, sin], dim=-1) 

511 query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) 

512 elif rotary_dim < head_dim: 

513 # Partial rotary (e.g., GPT-NeoX/Phi) where cos/sin cover only a 

514 # portion of head_dim. Split Q/K, rotate the partial dims, recombine. 

515 q_rot, q_pass = query_states[..., :rotary_dim], query_states[..., rotary_dim:] 

516 k_rot, k_pass = key_states[..., :rotary_dim], key_states[..., rotary_dim:] 

517 q_rot, k_rot = apply_rotary_pos_emb(q_rot, k_rot, cos, sin) 

518 query_states = torch.cat([q_rot, q_pass], dim=-1) 

519 key_states = torch.cat([k_rot, k_pass], dim=-1) 

520 else: 

521 query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin) 

522 

523 # Ministral-3's llama-4 query scale: HF multiplies Q by 

524 # 1 + beta*log(1 + floor(pos/original_max)) right after RoPE. 

525 rope_params = getattr(getattr(hf_attn, "config", None), "rope_parameters", None) 

526 if isinstance(rope_params, dict) and rope_params.get("llama_4_scaling_beta") is not None: 

527 from transformers.models.ministral3.modeling_ministral3 import ( 

528 get_llama_4_attn_scale, 

529 ) 

530 

531 position_ids = kwargs.get("position_ids") 

532 if position_ids is None: 532 ↛ 533line 532 didn't jump to line 533 because the condition on line 532 was never true

533 position_ids = torch.arange( 

534 query_states.shape[-2], device=query_states.device 

535 ).unsqueeze(0) 

536 query_states = query_states * get_llama_4_attn_scale( 

537 position_ids, 

538 rope_params["llama_4_scaling_beta"], 

539 int(rope_params["original_max_position_embeddings"]), 

540 ).to(query_states.dtype) 

541 

542 # Fire hook_rot_q/hook_rot_k (post-rotation) 

543 if hasattr(self, "hook_rot_q"): 

544 query_states = self.hook_rot_q(query_states) 

545 if hasattr(self, "hook_rot_k"): 

546 key_states = self.hook_rot_k(key_states) 

547 

548 # --- KV cache: extend K/V with cached positions --- 

549 key_states, value_states = self._update_kv_cache(key_states, value_states, **kwargs) 

550 

551 # --- GQA: Expand K/V --- 

552 num_key_value_groups = getattr(hf_attn, "num_key_value_groups", 1) 

553 if num_key_value_groups > 1: 

554 from transformers.models.llama.modeling_llama import repeat_kv 

555 

556 key_states_expanded = repeat_kv(key_states, num_key_value_groups) 

557 value_states_expanded = repeat_kv(value_states, num_key_value_groups) 

558 else: 

559 key_states_expanded = key_states 

560 value_states_expanded = value_states 

561 

562 # --- Attention Scores --- 

563 scaling = getattr(hf_attn, "scaling", head_dim**-0.5) 

564 attn_scores = torch.matmul(query_states, key_states_expanded.transpose(-2, -1)) * scaling 

565 

566 # --- Softcapping (Gemma 2) --- 

567 softcap = getattr(hf_attn, "attn_logit_softcapping", None) 

568 if softcap is not None: 

569 attn_scores = attn_scores / softcap 

570 attn_scores = torch.tanh(attn_scores) 

571 attn_scores = attn_scores * softcap 

572 

573 # --- Causal / Sliding Window Mask --- 

574 kv_seq_len = key_states_expanded.shape[-2] 

575 q_seq_len = query_states.shape[-2] 

576 attn_scores = self._apply_reconstruct_attention_mask( 

577 attn_scores=attn_scores, 

578 attention_mask=attention_mask, 

579 seq_len=kv_seq_len, 

580 q_seq_len=q_seq_len, 

581 ) 

582 

583 # --- hook_attn_scores: PRE-softmax (matching HookedTransformer) --- 

584 attn_scores = self.hook_attn_scores(attn_scores) 

585 

586 # --- Softmax (in float32 for numerical stability) --- 

587 # GPT-OSS attention sinks: a learned per-head logit joins the softmax as 

588 # an extra key column and is dropped afterward, so every real position's 

589 # weight is scaled down by the sink's share. Appended after 

590 # hook_attn_scores so hooks keep the [batch, head, q_pos, kv_pos] shape 

591 # and score patches still flow into the softmax. 

592 sinks = getattr(hf_attn, "sinks", None) 

593 if sinks is not None: 

594 sink_col = ( 

595 sinks.reshape(1, -1, 1, 1) 

596 .expand(attn_scores.shape[0], -1, attn_scores.shape[-2], -1) 

597 .to(attn_scores.dtype) 

598 ) 

599 combined = torch.cat([attn_scores, sink_col], dim=-1) 

600 combined = combined - combined.max(dim=-1, keepdim=True).values 

601 attn_weights = torch.nn.functional.softmax(combined, dim=-1, dtype=torch.float32).to( 

602 query_states.dtype 

603 )[..., :-1] 

604 else: 

605 attn_weights = torch.nn.functional.softmax(attn_scores, dim=-1, dtype=torch.float32).to( 

606 query_states.dtype 

607 ) 

608 attn_weights = self._scrub_compatibility_pattern_nans(attn_weights) 

609 

610 # --- Dropout --- 

611 dropout_rate = getattr(hf_attn, "attention_dropout", 0.0) 

612 if self.training and dropout_rate > 0.0: 612 ↛ 613line 612 didn't jump to line 613 because the condition on line 612 was never true

613 attn_weights = torch.nn.functional.dropout(attn_weights, p=dropout_rate, training=True) 

614 

615 # --- hook_pattern: POST-softmax --- 

616 attn_weights = self.hook_pattern(attn_weights) 

617 

618 # --- Attention Output --- 

619 attn_output = torch.matmul(attn_weights, value_states_expanded) 

620 attn_output = attn_output.transpose(1, 2).contiguous() 

621 attn_output = attn_output.reshape(*input_shape, -1) 

622 

623 # --- Gated attention (Qwen3.5/Qwen3Next) --- 

624 if q_gate is not None: 

625 if hasattr(self, "hook_q_gate"): 625 ↛ 627line 625 didn't jump to line 627 because the condition on line 625 was always true

626 q_gate = self.hook_q_gate(q_gate) 

627 attn_output = attn_output * torch.sigmoid(q_gate) 

628 

629 # --- Gated attention (HRM-Text: separate gate_proj on hidden_states) --- 

630 gate_comp = self._modules.get("gate") 

631 if gate_comp is not None and gate_comp.original_component is not None and q_gate is None: 

632 gate_states = gate_comp(hidden_states) 

633 if hasattr(self, "hook_gate"): 633 ↛ 635line 633 didn't jump to line 635 because the condition on line 633 was always true

634 gate_states = self.hook_gate(gate_states) 

635 attn_output = attn_output * torch.sigmoid(gate_states) 

636 

637 # Adapter seam: sub-layer transforms between attention output and the 

638 # o projection (e.g. BitNet's attn_sub_norm). 

639 attn_output = self._pre_output_projection(attn_output) 

640 

641 # Some rotary modules (FlexOlmo) return fp32 cos/sin without casting to 

642 # the input dtype, so RoPE promotes q/k 

643 # to fp32 while the projection weights stay in the model dtype. Match 

644 # the projection rather than the activations; no-op when they agree. 

645 o_module = getattr(self, "o", None) 

646 o_weight = getattr(getattr(o_module, "original_component", None), "weight", None) 

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

648 isinstance(o_weight, torch.Tensor) 

649 # Quantized weights (bnb int8/uint8, GPTQ int32) dequantize inside 

650 # the matmul; casting activations to an integer storage dtype would 

651 # destroy them (same guard as base.py's compute-dtype selection). 

652 and o_weight.dtype.is_floating_point 

653 and attn_output.is_floating_point() 

654 and attn_output.dtype != o_weight.dtype 

655 ): 

656 attn_output = attn_output.to(dtype=o_weight.dtype) 

657 

658 if ( 

659 bool(getattr(self.config, "use_attn_result", False)) 

660 and hasattr(self, "o") 

661 and self.o.original_component is not None 

662 ): 

663 # Per-head output pre-sum across heads. Fire hook_z on the pre- 

664 # projection tensor first so any patch at hook_z flows into the 

665 # per-head computation below — matches the default path where 

666 # `self.o(attn_output)` calls o.hook_in before the linear. 

667 n_heads = int(getattr(self.config, "n_heads")) 

668 attn_output = self.o.hook_in(attn_output) 

669 z_4d = attn_output.view(*input_shape, n_heads, head_dim) 

670 attn_output = self._compute_per_head_result(z_4d, n_heads, head_dim) 

671 attn_output = self.hook_out(attn_output) 

672 else: 

673 # Route through LinearBridge so hook_z (aliased to o.hook_in) fires. 

674 # LinearBridge wraps whichever HF attr the adapter mapped (o_proj, 

675 # dense, out_proj). 

676 attn_output = self.o(attn_output) 

677 attn_output = self.hook_out(attn_output) 

678 

679 return attn_output, attn_weights 

680 

681 def _pre_output_projection(self, attn_output: torch.Tensor) -> torch.Tensor: 

682 """Overridable seam applied before the output projection.""" 

683 return attn_output 

684 

685 def get_random_inputs( 

686 self, 

687 batch_size: int = 2, 

688 seq_len: int = 8, 

689 device: Optional[torch.device] = None, 

690 dtype: Optional[torch.dtype] = None, 

691 ) -> Dict[str, Any]: 

692 """Generate random inputs for Gemma-3 attention testing. 

693 

694 Gemma-3's position_embeddings are generated by calling rotary_emb(seq_len, device) 

695 which returns a tuple of (cos, sin) tensors with shape [seq_len, head_dim]. 

696 

697 Args: 

698 batch_size: Batch size for generated inputs 

699 seq_len: Sequence length for generated inputs 

700 device: Device to place tensors on 

701 dtype: Dtype for generated tensors 

702 

703 Returns: 

704 Dictionary with keys: hidden_states, position_embeddings, attention_mask 

705 """ 

706 if device is None: 

707 device = torch.device("cpu") 

708 if dtype is None: 

709 dtype = torch.float32 

710 d_model = self.config.d_model if self.config and hasattr(self.config, "d_model") else 1152 

711 inputs: Dict[str, Any] = { 

712 "hidden_states": torch.randn(batch_size, seq_len, d_model, device=device, dtype=dtype) 

713 } 

714 num_heads = safe_config_get(self.config, "num_attention_heads", 4) if self.config else 4 

715 head_dim = safe_config_get(self.config, "head_dim", 256) if self.config else 256 

716 dummy_qk = torch.randn(1, seq_len, num_heads, head_dim, device=device, dtype=dtype) 

717 position_ids = torch.arange(seq_len, device=device).unsqueeze(0) 

718 if self._rotary_emb is not None: 

719 try: 

720 position_embeddings = self._rotary_emb(dummy_qk, position_ids) 

721 inputs["position_embeddings"] = position_embeddings 

722 except Exception as e: 

723 cos = torch.ones(1, seq_len, head_dim, device=device, dtype=dtype) 

724 sin = torch.zeros(1, seq_len, head_dim, device=device, dtype=dtype) 

725 inputs["position_embeddings"] = (cos, sin) 

726 else: 

727 cos = torch.ones(1, seq_len, head_dim, device=device, dtype=dtype) 

728 sin = torch.zeros(1, seq_len, head_dim, device=device, dtype=dtype) 

729 inputs["position_embeddings"] = (cos, sin) 

730 inputs["attention_mask"] = None 

731 return inputs