Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / modules / mla_attention.py: 93%

117 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""Multi-head latent attention module using the Hyper projection layout.""" 

16 

17from __future__ import annotations 

18 

19from collections.abc import Callable, Mapping, Sequence 

20from dataclasses import dataclass 

21from typing import Any 

22 

23# This package provides PyTorch-specific high-performance modules. 

24# pylint: disable=forbidden-backend-import 

25import torch # pylint: disable=forbidden-backend-import 

26from torch import nn 

27from hyper_parallel.components.checkpoint.weight_conversion import WeightConverter 

28 

29from hyper_parallel.components.checkpoint import ConcatenateWithSections 

30from hyper_parallel.components.functional.npu_fusion_attention import npu_fusion_attention_forward 

31from hyper_parallel.models.replacement import module_replacement 

32 

33 

34def prepare_mla_attention_mask( 

35 attention_mask: torch.Tensor | None, 

36 query_length: int, 

37 key_length: int, 

38) -> torch.Tensor | None: 

39 """Disambiguate MLA Q/K masks before calling a shared attention interface. 

40 

41 Args: 

42 attention_mask: Optional mask. A two-dimensional Q/K matrix takes 

43 precedence over padding in MLA, including when batch equals query length. 

44 query_length: Number of query tokens visible to the attention interface. 

45 key_length: Number of key tokens visible to the attention interface. 

46 

47 Returns: 

48 A broadcastable four-dimensional view for a Q/K matrix. Other inputs 

49 pass through unchanged, preserving padding and compressed causal paths. 

50 """ 

51 if (attention_mask is not None and attention_mask.ndim == 2 

52 and attention_mask.shape == (query_length, key_length)): 

53 # Shared FA interprets 2D masks as batch padding; MLA also accepts Q/K matrices. 

54 return attention_mask[None, None, :, :] 

55 return attention_mask 

56 

57 

58@dataclass 

59class _MLALatents: 

60 batch_size: int 

61 sequence_length: int 

62 query_pass: torch.Tensor 

63 query_rope: torch.Tensor 

64 kv_nope: torch.Tensor 

65 key_rope: torch.Tensor 

66 

67 

68@module_replacement 

69class MLAAttention(nn.Module): 

70 """Transformers-compatible MLA using a fused latent projection. 

71 

72 ``q_a_proj`` and ``kv_a_proj_with_mqa`` are fused into ``linear_qkv``. 

73 Unchanged child modules retain their source names and checkpoint layout. 

74 Construction only creates the target structure; ``make_transforms`` 

75 declares the checkpoint conversion. 

76 """ 

77 

78 def __init__( 

79 self, 

80 *, 

81 module: nn.Module, 

82 module_fqn: str = "", 

83 context: Mapping[str, Any] | None = None, 

84 attention_interface: Callable[..., tuple[torch.Tensor, torch.Tensor | None]] = ( 

85 npu_fusion_attention_forward 

86 ), 

87 ) -> None: 

88 """Build the high-performance module from a Transformers MLA module.""" 

89 super().__init__() 

90 del module_fqn, context 

91 self.attention_interface = attention_interface 

92 required = ( 

93 "q_a_proj", 

94 "q_a_layernorm", 

95 "q_b_proj", 

96 "kv_a_proj_with_mqa", 

97 "kv_a_layernorm", 

98 "kv_b_proj", 

99 "o_proj", 

100 ) 

101 missing = [name for name in required if not hasattr(module, name)] 

102 if missing: 

103 raise TypeError(f"MLAAttention source module is missing: {missing}") 

104 

105 config = module.config 

106 self.config = config 

107 self.layer_idx = getattr(module, "layer_idx", getattr(module, "layer_number", None)) 

108 self.num_heads = getattr(module, "num_heads", config.num_attention_heads) 

109 self.num_key_value_heads = getattr( 

110 module, 

111 "num_key_value_heads", 

112 getattr(config, "num_key_value_heads", self.num_heads), 

113 ) 

114 if self.num_key_value_heads is None: 

115 self.num_key_value_heads = self.num_heads 

116 self.num_key_value_groups = self.num_heads // self.num_key_value_heads 

117 self.q_lora_rank = getattr(module, "q_lora_rank", config.q_lora_rank) 

118 self.kv_lora_rank = getattr(module, "kv_lora_rank", config.kv_lora_rank) 

119 if self.q_lora_rank is None: 

120 raise ValueError("MLAAttention requires the Q-LoRA projection path") 

121 self.qk_rope_head_dim = getattr(module, "qk_rope_head_dim", config.qk_rope_head_dim) 

122 self.qk_nope_head_dim = getattr(module, "qk_nope_head_dim", config.qk_nope_head_dim) 

123 self.v_head_dim = getattr(module, "v_head_dim", config.v_head_dim) 

124 self.qk_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim 

125 self.scaling = getattr(module, "scaling", self.qk_head_dim**-0.5) 

126 self.attention_dropout = getattr(module, "attention_dropout", 0.0) 

127 if isinstance(self.attention_dropout, nn.Dropout): 

128 self.attention_dropout = self.attention_dropout.p 

129 self.is_causal = getattr(module, "is_causal", True) 

130 self.sliding_window = getattr( 

131 module, "sliding_window", getattr(config, "sliding_window", None) 

132 ) 

133 self.rotary_interleaved = bool( 

134 getattr(module, "rotary_interleaved", getattr(config, "rope_interleave", False)) 

135 ) 

136 if getattr(module, "param_sink_number", 0) > 0: 

137 raise ValueError( 

138 "MLAAttention only implements standard MLA; parameter-sink attention " 

139 "must use the sink high-performance interfaces." 

140 ) 

141 self.use_mome = bool(getattr(module, "use_mome", False)) 

142 if self.use_mome: 

143 raise NotImplementedError( 

144 "MLAAttention only implements standard MLA; MOME must use the " 

145 "aggregate_hidden high-performance path." 

146 ) 

147 

148 q_a = module.q_a_proj 

149 kv_a = module.kv_a_proj_with_mqa 

150 self._q_latent_output_size = q_a.out_features 

151 self._kv_latent_output_size = kv_a.out_features 

152 can_fuse = ( 

153 q_a.in_features == kv_a.in_features 

154 and q_a.weight.requires_grad == kv_a.weight.requires_grad 

155 and (q_a.bias is None) == (kv_a.bias is None) 

156 and (q_a.bias is None or q_a.bias.requires_grad == kv_a.bias.requires_grad) 

157 ) 

158 if not can_fuse: 

159 raise ValueError( 

160 "Q and KV latent projections cannot be represented by one fused projection" 

161 ) 

162 self.linear_qkv = nn.Linear( 

163 q_a.in_features, 

164 q_a.out_features + kv_a.out_features, 

165 bias=q_a.bias is not None, 

166 device=q_a.weight.device, 

167 dtype=q_a.weight.dtype, 

168 ) 

169 self.linear_qkv.weight.requires_grad_(q_a.weight.requires_grad) 

170 if q_a.bias is not None: 

171 self.linear_qkv.bias.requires_grad_(q_a.bias.requires_grad) 

172 

173 self.q_a_layernorm = module.q_a_layernorm 

174 self.kv_a_layernorm = module.kv_a_layernorm 

175 self.q_b_proj = module.q_b_proj 

176 self.kv_b_proj = module.kv_b_proj 

177 self.o_proj = module.o_proj 

178 

179 self.train(module.training) 

180 

181 def make_transforms(self) -> list[WeightConverter]: 

182 """Describe reversible source-checkpoint to high-performance conversion.""" 

183 transforms: list[WeightConverter] = [ 

184 WeightConverter( 

185 source_patterns=[ 

186 "q_a_proj.weight", 

187 "kv_a_proj_with_mqa.weight", 

188 ], 

189 target_patterns="linear_qkv.weight", 

190 operations=[ 

191 ConcatenateWithSections( 

192 sections=( 

193 self._q_latent_output_size, 

194 self._kv_latent_output_size, 

195 ), 

196 dim=0, 

197 ) 

198 ], 

199 ), 

200 ] 

201 if self.linear_qkv.bias is not None: 

202 transforms.insert( 

203 1, 

204 WeightConverter( 

205 source_patterns=[ 

206 "q_a_proj.bias", 

207 "kv_a_proj_with_mqa.bias", 

208 ], 

209 target_patterns="linear_qkv.bias", 

210 operations=[ 

211 ConcatenateWithSections( 

212 sections=( 

213 self._q_latent_output_size, 

214 self._kv_latent_output_size, 

215 ), 

216 dim=0, 

217 ) 

218 ], 

219 ), 

220 ) 

221 return transforms 

222 

223 def project_latent_inputs( 

224 self, hidden_states: torch.Tensor, 

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

226 """Compute shared latent inputs before head expansion. 

227 

228 Args: 

229 hidden_states: Input tokens with shape [batch, sequence, hidden]. 

230 

231 Returns: 

232 Normalized query and KV latents, and the unrotated shared RoPE key. 

233 """ 

234 latent_states = self.linear_qkv(hidden_states) 

235 query_latent, kv_nope, key_rope = torch.split( 

236 latent_states, 

237 (self.q_lora_rank, self.kv_lora_rank, self.qk_rope_head_dim), 

238 dim=-1, 

239 ) 

240 return self.q_a_layernorm(query_latent), self.kv_a_layernorm(kv_nope), key_rope 

241 

242 def _project_latents(self, hidden_states: torch.Tensor) -> _MLALatents: 

243 """Project hidden states into query and compressed KV latent states.""" 

244 batch_size, sequence_length = hidden_states.shape[:-1] 

245 query_latent, kv_nope, key_rope = self.project_latent_inputs(hidden_states) 

246 query_states = self.q_b_proj(query_latent).view( 

247 batch_size, 

248 sequence_length, 

249 self.num_heads, 

250 self.qk_head_dim, 

251 ) 

252 query_pass, query_rope = torch.split( 

253 query_states, 

254 (self.qk_nope_head_dim, self.qk_rope_head_dim), 

255 dim=-1, 

256 ) 

257 return _MLALatents( 

258 batch_size, 

259 sequence_length, 

260 query_pass, 

261 query_rope.transpose(1, 2), 

262 kv_nope.view( 

263 batch_size, 1, sequence_length, self.kv_lora_rank 

264 ), 

265 key_rope.view(batch_size, 1, sequence_length, self.qk_rope_head_dim), 

266 ) 

267 

268 def _project_attention_inputs( 

269 self, 

270 hidden_states: torch.Tensor, 

271 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None, 

272 past_key_values: Any | None, 

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

274 """Build query, key, and value tensors from compressed latent states.""" 

275 latents = self._project_latents(hidden_states) 

276 query_rope, key_rope = latents.query_rope, latents.key_rope 

277 if position_embeddings is not None: 

278 # Defer the optional NPU dependency until rotary embedding is used. 

279 from hyper_parallel.components.functional import ( # pylint: disable=C0415 

280 apply_rotary_pos_emb, apply_rotary_pos_emb_interleave, 

281 ) 

282 

283 cos, sin = position_embeddings 

284 rope_fn = apply_rotary_pos_emb_interleave if self.rotary_interleaved else apply_rotary_pos_emb 

285 query_rope, key_rope = rope_fn(query_rope, key_rope, cos, sin) 

286 kv_nope = latents.kv_nope 

287 if past_key_values is not None: 

288 kv_nope, key_rope = past_key_values.update(kv_nope, key_rope, self.layer_idx) 

289 kv_states = self.kv_b_proj(kv_nope).view( 

290 latents.batch_size, 

291 kv_nope.shape[2], 

292 self.num_heads, 

293 self.qk_nope_head_dim + self.v_head_dim, 

294 ).transpose(1, 2) 

295 key_nope, value_states = torch.split( 

296 kv_states, 

297 (self.qk_nope_head_dim, self.v_head_dim), 

298 dim=-1, 

299 ) 

300 return ( 

301 torch.cat((latents.query_pass.transpose(1, 2), query_rope), dim=-1), 

302 torch.cat((key_nope, key_rope.expand(-1, self.num_heads, -1, -1)), dim=-1), 

303 value_states, 

304 ) 

305 

306 def project_output(self, attention_output: torch.Tensor, hidden_states: torch.Tensor) -> torch.Tensor: 

307 """Project [B, local_tokens, heads * value_dim] to [B, local_tokens, hidden]. 

308 

309 This model-math extension point lets gated MLA multiply its gate before 

310 Wo; the default MLA keeps the original output projection unchanged. 

311 This method owns model math only; CP communication belongs to its adapter. 

312 

313 Args: 

314 attention_output: Flattened local-token attention output. 

315 hidden_states: Original local-token input, available to gated variants. 

316 

317 Returns: 

318 Projected local hidden states. 

319 """ 

320 del hidden_states 

321 return self.o_proj(attention_output) 

322 

323 def forward( 

324 self, 

325 hidden_states: torch.Tensor, 

326 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None, 

327 attention_mask: torch.Tensor | None = None, 

328 past_key_values: Any | None = None, 

329 actual_seq_len: torch.Tensor | Sequence[int] | None = None, 

330 **kwargs: Any, 

331 ) -> tuple[torch.Tensor, torch.Tensor | None]: 

332 """Run MLA with the same external contract as Transformers attention. 

333 

334 Args: 

335 hidden_states: Input tokens in batch, sequence, hidden order. 

336 position_embeddings: Optional precomputed RoPE cosine and sine. 

337 attention_mask: Optional mask in the attention interface convention. 

338 past_key_values: Optional cache for incremental decoding. 

339 actual_seq_len: Optional cumulative packed document ends. 

340 **kwargs: Additional arguments for the attention interface. 

341 

342 Returns: 

343 Projected attention output and optional attention weights. 

344 """ 

345 batch_size, seq_length = hidden_states.shape[:-1] 

346 query_states, key_states, value_states = self._project_attention_inputs( 

347 hidden_states, 

348 position_embeddings, 

349 past_key_values, 

350 ) 

351 

352 attention_mask = prepare_mla_attention_mask( 

353 attention_mask, query_states.shape[2], key_states.shape[2], 

354 ) 

355 attn_output, attn_weights = self.attention_interface( 

356 self, 

357 query_states, 

358 key_states, 

359 value_states, 

360 attention_mask, 

361 dropout=self.attention_dropout if self.training else 0.0, 

362 scaling=self.scaling, 

363 sliding_window=self.sliding_window, 

364 actual_seq_len=actual_seq_len, 

365 **kwargs, 

366 ) 

367 attn_output = attn_output.reshape(batch_size, seq_length, -1).contiguous() 

368 # Gated MLA derives its gate from the original input before Wo; ordinary 

369 # MLA only applies Wo. Keep this math extension shared by CP1 and CP. 

370 attn_output = self.project_output(attn_output, hidden_states) 

371 return attn_output, attn_weights