Coverage for transformer_lens/model_bridge/generalized_components/mla_attention.py: 67%

194 statements  

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

1"""Multi-Head Latent Attention (MLA) bridge component for DeepSeek models. 

2 

3MLA compresses Q and KV into lower-dimensional latent spaces via LoRA-style 

4projections before standard attention. This component reimplements the MLA 

5forward path step-by-step with hooks at each meaningful stage, exposing: 

6 

7- hook_q_latent / hook_kv_latent: compressed representations (the information bottleneck) 

8- hook_q / hook_k / hook_v: final Q/K/V entering attention (post-decompression, post-RoPE) 

9- hook_rot_q / hook_rot_k: after RoPE on the rope portion splits 

10- hook_attn_scores / hook_pattern: pre/post-softmax attention weights 

11- hook_z: pre-output-projection (alias for o.hook_in) 

12- hook_result: per-head output (z_h @ W_O_h, pre-sum), gated by use_attn_result 

13""" 

14 

15from __future__ import annotations 

16 

17from typing import Any, Dict, Optional 

18 

19import torch 

20 

21from transformer_lens.hook_points import HookPoint 

22from transformer_lens.model_bridge.generalized_components.attention import ( 

23 AttentionBridge, 

24) 

25from transformer_lens.model_bridge.generalized_components.base import ( 

26 GeneralizedComponent, 

27) 

28from transformer_lens.model_bridge.generalized_components.position_embedding_hooks_mixin import ( 

29 PositionEmbeddingHooksMixin, 

30) 

31 

32 

33def _rotate_half(x: torch.Tensor) -> torch.Tensor: 

34 """Rotate half of the hidden dims of the input (standard RoPE helper).""" 

35 x1 = x[..., : x.shape[-1] // 2] 

36 x2 = x[..., x.shape[-1] // 2 :] 

37 return torch.cat((-x2, x1), dim=-1) 

38 

39 

40def _apply_rotary_pos_emb( 

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

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

43 """Apply rotary position embedding to q and k tensors.""" 

44 cos = cos.unsqueeze(1) # [batch, 1, seq, dim] 

45 sin = sin.unsqueeze(1) 

46 q_embed = (q * cos) + (_rotate_half(q) * sin) 

47 k_embed = (k * cos) + (_rotate_half(k) * sin) 

48 return q_embed, k_embed 

49 

50 

51def _apply_rotary_pos_emb_interleave( 

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

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

54 """Apply interleaved-pair rotary position embedding (transformers >= 5.13). 

55 

56 Pairs even-dimension elements with the following odd dimension, matching 

57 HF's ``apply_rotary_pos_emb_interleave`` (used by GLM-MoE-DSA and DeepSeek-V3 

58 when ``rope_interleave=True``). 

59 """ 

60 cos = cos[..., : cos.shape[-1] // 2].unsqueeze(1) 

61 sin = sin[..., : sin.shape[-1] // 2].unsqueeze(1) 

62 q1, q2 = q[..., 0::2], q[..., 1::2] 

63 k1, k2 = k[..., 0::2], k[..., 1::2] 

64 q_embed = torch.cat([q1 * cos - q2 * sin, q2 * cos + q1 * sin], dim=-1) 

65 k_embed = torch.cat([k1 * cos - k2 * sin, k2 * cos + k1 * sin], dim=-1) 

66 return q_embed, k_embed 

67 

68 

69def _apply_rotary_complex( 

70 q: torch.Tensor, k: torch.Tensor, freqs_cis: torch.Tensor 

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

72 """Apply rotary position embedding via complex multiplication (DeepSeek-V2 style). 

73 

74 DeepSeek-V2 uses ``freqs_cis = torch.polar(ones, freqs)`` (complex exponentials) 

75 instead of the standard (cos, sin) pair. This matches the V2 HF implementation of 

76 ``apply_rotary_emb``. 

77 

78 Args: 

79 q: Query rope portion [batch, heads, seq, rope_dim]. 

80 k: Key rope portion [batch, 1, seq, rope_dim]. 

81 freqs_cis: Complex rotary frequencies [batch, seq, rope_dim // 2]. 

82 

83 Returns: 

84 Tuple of rotated (q, k) tensors with same dtype and shape as inputs. 

85 """ 

86 freqs = freqs_cis.unsqueeze(1) # [batch, 1, seq, rope_dim // 2] 

87 q_c = torch.view_as_complex(q.float().reshape(*q.shape[:-1], -1, 2)) 

88 k_c = torch.view_as_complex(k.float().reshape(*k.shape[:-1], -1, 2)) 

89 q_rot = torch.view_as_real(q_c * freqs.to(q_c.device)).flatten(3).type_as(q) 

90 k_rot = torch.view_as_real(k_c * freqs.to(k_c.device)).flatten(3).type_as(k) 

91 return q_rot, k_rot 

92 

93 

94class MLAAttentionBridge(PositionEmbeddingHooksMixin, AttentionBridge): 

95 """Bridge for DeepSeek's Multi-Head Latent Attention (MLA). 

96 

97 Reimplements the MLA forward path with hooks at each computation stage. 

98 Standard W_Q/W_K/W_V properties are not available on MLA models — use 

99 the submodule weight access (q_a_proj, q_b_proj, etc.) instead. 

100 """ 

101 

102 # MLA has no standard q/k/v submodules — override to empty 

103 property_aliases: Dict[str, str] = {} 

104 

105 hook_aliases = { 

106 "hook_z": "o.hook_in", 

107 } 

108 

109 # MLA's forward never forks the residual pre-LN; suppress dead HookPoints. 

110 supports_split_qkv_fork: bool = False 

111 # o_proj is a plain concat-heads projection, so z_h @ W_O_h is well-defined. 

112 supports_attn_result: bool = True 

113 

114 def __init__( 

115 self, 

116 name: str, 

117 config: Any, 

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

119 **kwargs: Any, 

120 ): 

121 super().__init__(name, config, submodules=submodules, **kwargs) 

122 self._init_position_embedding_hooks() 

123 # The per-head hook_result computation lives in this class's forward; a 

124 # subclass that replaces forward (GLM DSA) does not inherit it, so it 

125 # must not advertise support it cannot honor. 

126 if type(self).forward is not MLAAttentionBridge.forward: 

127 self.supports_attn_result = False 

128 

129 self.hook_q_latent = HookPoint() # Compressed Q (post q_a_layernorm) 

130 self.hook_kv_latent = HookPoint() # Compressed KV (post kv_a_layernorm) 

131 self.hook_q = HookPoint() # Final Q entering attention (post-RoPE concat) 

132 self.hook_k = HookPoint() # Final K entering attention (post-RoPE concat) 

133 self.hook_v = HookPoint() # V from kv_b_proj split 

134 self.hook_rot_q = HookPoint() # Q rope portion after RoPE 

135 self.hook_rot_k = HookPoint() # K rope portion after RoPE 

136 # hook_q/k/v fire after the head transpose, on [batch, heads, pos, d]; the 

137 # rotary hooks are transposed back to [batch, pos, heads, d] for the user 

138 # by the TransposeRotaryHeads conversion, so they keep the default axis. 

139 for hook in (self.hook_q, self.hook_k, self.hook_v): 

140 hook.pos_dim = 2 

141 

142 # MLA params lazy-initialized from HF module (bridge config lacks these fields) 

143 self._mla_params_initialized = False 

144 

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

146 """Reimplemented MLA forward with hooks at each computation stage. 

147 

148 Follows the DeepseekV3Attention forward path, calling into HF submodules 

149 individually and firing hooks at each meaningful stage. 

150 """ 

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

152 raise RuntimeError( 

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

154 "Call set_original_component() first." 

155 ) 

156 

157 hf_attn: Any = self.original_component 

158 

159 if not self._mla_params_initialized: 

160 self._q_lora_rank = getattr(hf_attn, "q_lora_rank", None) 

161 self._kv_lora_rank = getattr(hf_attn, "kv_lora_rank", 512) 

162 self._qk_nope_head_dim = getattr(hf_attn, "qk_nope_head_dim", 128) 

163 self._qk_rope_head_dim = getattr(hf_attn, "qk_rope_head_dim", 64) 

164 self._v_head_dim = getattr(hf_attn, "v_head_dim", 128) 

165 self._qk_head_dim = self._qk_nope_head_dim + self._qk_rope_head_dim 

166 self._n_heads = getattr(hf_attn, "num_heads", 32) 

167 hf_config = getattr(hf_attn, "config", None) 

168 self._rope_interleave = ( 

169 getattr(hf_config, "rope_interleave", False) if hf_config else False 

170 ) 

171 self._mla_params_initialized = True 

172 

173 # --- Extract inputs --- 

174 if "hidden_states" in kwargs: 

175 hidden_states = kwargs.pop("hidden_states") 

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

177 hidden_states = args[0] 

178 args = args[1:] 

179 else: 

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

181 

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

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

184 

185 hidden_states = self.hook_in(hidden_states) 

186 

187 batch_size, seq_length = hidden_states.shape[:2] 

188 

189 # --- Query path --- 

190 if self._q_lora_rank is None: 

191 # Direct projection (no compression) 

192 q_states = hf_attn.q_proj(hidden_states) 

193 else: 

194 # Two-stage compression: q_a_proj → q_a_layernorm → q_b_proj 

195 q_compressed = hf_attn.q_a_proj(hidden_states) 

196 q_compressed = hf_attn.q_a_layernorm(q_compressed) 

197 q_compressed = self.hook_q_latent(q_compressed) 

198 q_states = hf_attn.q_b_proj(q_compressed) 

199 

200 # Reshape to [batch, n_heads, seq, qk_head_dim] 

201 q_states = q_states.view(batch_size, seq_length, -1, self._qk_head_dim).transpose(1, 2) 

202 # Split into nope (non-RoPE) and pe (RoPE) portions 

203 q_pass, q_rot = torch.split( 

204 q_states, [self._qk_nope_head_dim, self._qk_rope_head_dim], dim=-1 

205 ) 

206 

207 # --- KV path --- 

208 # kv_a_proj_with_mqa outputs [compressed_kv || k_pe] 

209 compressed_kv_full = hf_attn.kv_a_proj_with_mqa(hidden_states) 

210 # Split: compressed KV latent (for kv_b_proj) and k rope portion (for direct RoPE) 

211 # Note: k_pe is split off here and goes directly to RoPE — hook_kv_latent 

212 # captures only the compressed_kv portion that enters the decompression path. 

213 k_pass, k_rot = torch.split( 

214 compressed_kv_full, [self._kv_lora_rank, self._qk_rope_head_dim], dim=-1 

215 ) 

216 

217 # Compress → normalize → decompress the KV latent 

218 k_pass = hf_attn.kv_a_layernorm(k_pass) 

219 k_pass = self.hook_kv_latent(k_pass) 

220 k_pass = hf_attn.kv_b_proj(k_pass) 

221 

222 # Reshape to [batch, n_heads, seq, nope+v_head] 

223 key_shape = (batch_size, seq_length, -1, self._qk_nope_head_dim + self._v_head_dim) 

224 k_pass = k_pass.view(key_shape).transpose(1, 2) 

225 # Split K nope portion and V 

226 k_pass, value_states = torch.split( 

227 k_pass, [self._qk_nope_head_dim, self._v_head_dim], dim=-1 

228 ) 

229 

230 # k_rot is [batch, seq, rope_dim] → [batch, 1, seq, rope_dim] for broadcasting 

231 k_rot = k_rot.view(batch_size, 1, seq_length, self._qk_rope_head_dim) 

232 

233 # --- RoPE --- 

234 # DeepSeek-V2 passes a complex freqs_cis tensor; V3 passes a (cos, sin) tuple. 

235 # Detect the format and apply the appropriate rotation. 

236 cos = sin = None 

237 if position_embeddings is not None: 237 ↛ 251line 237 didn't jump to line 251 because the condition on line 237 was always true

238 position_embeddings = self._apply_position_embedding_hooks(position_embeddings) 

239 if isinstance(position_embeddings, torch.Tensor) and position_embeddings.is_complex(): 

240 # V2-style: complex exponential freqs_cis 

241 q_rot, k_rot = _apply_rotary_complex(q_rot, k_rot, position_embeddings) 

242 elif self._rope_interleave: 242 ↛ 246line 242 didn't jump to line 246 because the condition on line 242 was always true

243 cos, sin = position_embeddings 

244 q_rot, k_rot = _apply_rotary_pos_emb_interleave(q_rot, k_rot, cos, sin) 

245 else: 

246 cos, sin = position_embeddings 

247 if self._rope_interleave: 

248 q_rot, k_rot = _apply_rotary_pos_emb_interleave(q_rot, k_rot, cos, sin) 

249 else: 

250 q_rot, k_rot = _apply_rotary_pos_emb(q_rot, k_rot, cos, sin) 

251 elif self._rotary_emb is not None: 

252 # Fallback: compute from rotary_emb if position_embeddings not passed 

253 position_ids = torch.arange(seq_length, device=hidden_states.device).unsqueeze(0) 

254 emb = self._rotary_emb(hidden_states, position_ids) 

255 if isinstance(emb, torch.Tensor) and emb.is_complex(): 

256 q_rot, k_rot = _apply_rotary_complex(q_rot, k_rot, emb) 

257 elif self._rope_interleave: 

258 cos, sin = emb 

259 q_rot, k_rot = _apply_rotary_pos_emb_interleave(q_rot, k_rot, cos, sin) 

260 else: 

261 cos, sin = emb 

262 if self._rope_interleave: 

263 q_rot, k_rot = _apply_rotary_pos_emb_interleave(q_rot, k_rot, cos, sin) 

264 else: 

265 q_rot, k_rot = _apply_rotary_pos_emb(q_rot, k_rot, cos, sin) 

266 else: 

267 raise ValueError( 

268 "MLAAttentionBridge requires position_embeddings or set_rotary_emb() " 

269 "to be called before forward." 

270 ) 

271 q_rot = self.hook_rot_q(q_rot) 

272 k_rot = self.hook_rot_k(k_rot) 

273 

274 # Expand k_rot to match the number of heads 

275 k_rot = k_rot.expand(*k_pass.shape[:-1], -1) 

276 

277 # Concatenate nope + rope portions to form final Q and K 

278 query_states = torch.cat((q_pass, q_rot), dim=-1) 

279 key_states = torch.cat((k_pass, k_rot), dim=-1) 

280 

281 # Fire final Q/K/V hooks — these are the tensors entering attention 

282 query_states = self.hook_q(query_states) 

283 key_states = self.hook_k(key_states) 

284 value_states = self.hook_v(value_states) 

285 

286 # --- KV Cache --- 

287 past_key_values = kwargs.pop("past_key_values", None) 

288 cache_position = kwargs.pop("cache_position", None) 

289 if past_key_values is not None: 

290 cache_kwargs: dict = {"cache_position": cache_position} 

291 if cos is not None: 

292 cache_kwargs["cos"] = cos 

293 if sin is not None: 

294 cache_kwargs["sin"] = sin 

295 key_states, value_states = past_key_values.update( 

296 key_states, value_states, hf_attn.layer_idx, cache_kwargs 

297 ) 

298 

299 # --- Attention computation (no V padding — only needed for flash attention) --- 

300 # Read the HF module's scaling: DeepSeek-V2/V3 multiply the base 

301 # qk_head_dim^-0.5 by yarn mscale^2 under long-context rope configs. 

302 # trust_remote_code DeepSeek-V2 names the same quantity softmax_scale. 

303 scaling = getattr(hf_attn, "scaling", None) 

304 if scaling is None: 

305 scaling = getattr(hf_attn, "softmax_scale", None) 

306 if scaling is None: 

307 scaling = self._qk_head_dim ** (-0.5) 

308 attn_scores = torch.matmul(query_states, key_states.transpose(-2, -1)) * scaling 

309 

310 if attention_mask is not None: 

311 attention_mask = self._normalize_compatibility_mask_sentinel(attention_mask) 

312 attn_scores = attn_scores + attention_mask 

313 

314 attn_scores = self.hook_attn_scores(attn_scores) 

315 attn_weights = self._softmax_dropout_pattern( 

316 attn_scores, upcast_to_fp32=True, target_dtype=query_states.dtype 

317 ) 

318 

319 # Weighted sum of values 

320 attn_output = torch.matmul(attn_weights, value_states) 

321 

322 # --- Output projection --- 

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

324 attn_output = attn_output.reshape(batch_size, seq_length, -1) 

325 o_bridge = getattr(self, "o", None) 

326 if ( 

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

328 and isinstance(o_bridge, GeneralizedComponent) 

329 and o_bridge.original_component is not None 

330 ): 

331 # Per-head output pre-sum across heads. Fire hook_z (o.hook_in) on 

332 # the pre-projection tensor first so a patch there flows into the 

333 # per-head computation — matches the default path, where o_proj's 

334 # bridge fires it inside the projection call. 

335 attn_output = o_bridge.hook_in(attn_output) 

336 # Head count from the tensor, not cfg: remote-code V2 modules may 

337 # not expose num_heads, and the flat width is n_heads * v_head_dim 

338 # by construction. 

339 n_heads = attn_output.shape[-1] // self._v_head_dim 

340 z_4d = attn_output.reshape(batch_size, seq_length, n_heads, self._v_head_dim) 

341 attn_output = self._compute_per_head_result(z_4d, n_heads, self._v_head_dim) 

342 else: 

343 attn_output = hf_attn.o_proj(attn_output) 

344 

345 attn_output = self.hook_out(attn_output) 

346 return attn_output, attn_weights 

347 

348 def get_random_inputs( 

349 self, 

350 batch_size: int = 2, 

351 seq_len: int = 8, 

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

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

354 ) -> Dict[str, Any]: 

355 """Generate test inputs with hidden_states, position_embeddings, and attention_mask.""" 

356 if device is None: 

357 device = torch.device("cpu") 

358 if dtype is None: 

359 dtype = torch.float32 

360 

361 # Try bridge config (d_model), then HF attention's config (hidden_size), then fallback 

362 d_model = None 

363 if self.config and hasattr(self.config, "d_model"): 

364 d_model = self.config.d_model 

365 if d_model is None and self.original_component is not None: 

366 hf_cfg = getattr(self.original_component, "config", None) 

367 if hf_cfg is not None: 

368 d_model = getattr(hf_cfg, "hidden_size", None) 

369 if d_model is None: 

370 d_model = 256 

371 inputs: Dict[str, Any] = { 

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

373 } 

374 

375 # Generate position_embeddings from rotary_emb if available, 

376 # otherwise create dummy (cos=1, sin=0) embeddings 

377 rope_head_dim = self._qk_rope_head_dim if self._mla_params_initialized else 64 

378 if self._rotary_emb is not None: 

379 try: 

380 dummy_input = inputs["hidden_states"] 

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

382 position_embeddings = self._rotary_emb(dummy_input, position_ids) 

383 inputs["position_embeddings"] = position_embeddings 

384 except Exception: 

385 cos = torch.ones(1, seq_len, rope_head_dim, device=device, dtype=dtype) 

386 sin = torch.zeros(1, seq_len, rope_head_dim, device=device, dtype=dtype) 

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

388 else: 

389 cos = torch.ones(1, seq_len, rope_head_dim, device=device, dtype=dtype) 

390 sin = torch.zeros(1, seq_len, rope_head_dim, device=device, dtype=dtype) 

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

392 

393 inputs["attention_mask"] = None 

394 return inputs 

395 

396 def __getattr__(self, name: str) -> Any: 

397 """Raise clear error for standard weight properties that don't apply to MLA.""" 

398 if name in ("W_Q", "W_K", "W_V", "W_O", "b_Q", "b_K", "b_V", "b_O"): 

399 raise NotImplementedError( 

400 f"{name} is not available on MLA (Multi-Head Latent Attention) models. " 

401 f"MLA uses compressed projections instead of standard Q/K/V. " 

402 f"Access weights via submodules: q_a_proj, q_b_proj, kv_a_proj_with_mqa, " 

403 f"kv_b_proj, o (o_proj)." 

404 ) 

405 return super().__getattr__(name)