Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / modules / dsa_attention.py: 0%
405 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« 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"""Model-compatible DSA modules using the Hyper high-performance operators."""
17from __future__ import annotations
19from collections.abc import Mapping, Sequence
20from typing import Any
22# This package provides PyTorch-specific high-performance modules.
23# pylint: disable=forbidden-backend-import
24import torch # pylint: disable=forbidden-backend-import
25from torch import nn
26from torch.nn import functional as F
27from hyper_parallel.components.checkpoint.weight_conversion import WeightConverter
29from hyper_parallel.components.checkpoint import ConcatenateWithSections
30from hyper_parallel.models.replacement import module_replacement
31from hyper_parallel.components.functional import aggregate_hidden, aux_loss_auto_scale
32from hyper_parallel.components.functional import apply_rotary_pos_emb, apply_rotary_pos_emb_interleave
33from hyper_parallel.components.functional import (
34 dsa_indexer,
35 dsa_kl_loss,
36 dsa_sparse_attention,
37 dsa_sparse_attention_rescale,
38)
39from hyper_parallel.components.functional.npu_fusion_attention import resolve_packed_sequence_lengths
42def _dense_flash_attention(
43 module: nn.Module,
44 query: torch.Tensor,
45 key: torch.Tensor,
46 value: torch.Tensor,
47 sequence_lengths: tuple[torch.Tensor, torch.Tensor],
48 *,
49 need_stats: bool,
50) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
51 """Run one TND FA and return dense-indexer statistics."""
52 query_ends, key_ends = (lengths.tolist() for lengths in sequence_lengths)
53 q_flat, k_flat, v_flat = (tensor.flatten(0, 1) for tensor in (query, key, value))
54 num_heads = q_flat.shape[1]
55 causal_mask = torch.ones((2048, 2048), dtype=torch.bool, device=query.device).triu(diagonal=1)
56 import torch_npu # pylint: disable=import-outside-toplevel,forbidden-backend-import
58 result = torch_npu.npu_fusion_attention(
59 q_flat.contiguous(), k_flat.contiguous(), v_flat.contiguous(), num_heads, "TND",
60 pse=None, padding_mask=None, atten_mask=causal_mask, scale=module.scaling,
61 pre_tockens=getattr(module, "pre_tockens", 1048576),
62 next_tockens=getattr(module, "next_tockens", 0), keep_prob=1.0, inner_precise=0,
63 actual_seq_qlen=query_ends, actual_seq_kvlen=key_ends, sparse_mode=3,
64 )
65 output = result[0].reshape(*query.shape[:3], value.shape[-1])
66 if not need_stats:
67 return output, None, None
69 # TND FA stores each packed segment as [head, token, 8], while exposing a
70 # physical [token, head, 8] shape. Dense KL consumes [head, token, 1].
71 statistics = []
72 starts = [0, *query_ends[:-1]]
73 for statistic in result[1:3]:
74 rows = statistic.reshape(-1, statistic.shape[-1])
75 segments = [
76 rows[start * num_heads : end * num_heads].reshape(
77 num_heads, end - start, statistic.shape[-1]
78 )[..., :1]
79 for start, end in zip(starts, query_ends)
80 ]
81 statistics.append(torch.cat(segments, dim=1).detach())
82 return output, statistics[0], statistics[1]
85def _deepseek_v32_dense_attention(
86 module: nn.Module,
87 index_states: tuple[torch.Tensor, ...],
88 attention_states: tuple[torch.Tensor, ...],
89 sequence_lengths: tuple[torch.Tensor, torch.Tensor],
90) -> torch.Tensor:
91 """Run unabsorbed MLA FA and the fused dense indexer KL on local states."""
92 query, compressed_kv, query_rope, key_rope = attention_states
93 projected_kv = module.kv_b_proj(compressed_kv.squeeze(2)).view(
94 *compressed_kv.shape[:2], module.num_heads, module.qk_nope_head_dim + module.v_head_dim,
95 )
96 key, value = projected_kv.split((module.qk_nope_head_dim, module.v_head_dim), dim=-1)
97 key_rope = key_rope.expand(-1, -1, module.num_heads, -1)
98 need_stats = bool(module.training and not module.freeze_dsa and module.dsa_loss_coeff)
99 output, maximum, total = _dense_flash_attention(
100 module, torch.cat((query, query_rope), dim=-1), torch.cat((key, key_rope), dim=-1),
101 value, sequence_lengths, need_stats=need_stats,
102 )
103 if need_stats:
104 # Dense CANN operators are optional for sparse and Omni execution.
105 # pylint: disable=import-outside-toplevel
106 from hyper_parallel.components.functional.dsa_dense import (
107 dsa_dense_indexer_kl_loss,
108 dsa_dense_indexer_lse,
109 )
110 # pylint: enable=import-outside-toplevel
112 query_lengths, key_lengths = sequence_lengths
113 index_query, index_key, weights = (tensor.flatten(0, 1) for tensor in index_states)
114 indexer_stats = dsa_dense_indexer_lse(
115 index_query, index_key, weights, query_lengths=query_lengths, key_lengths=key_lengths,
116 )
117 loss = dsa_dense_indexer_kl_loss(
118 index_query, index_key, weights, query.flatten(0, 1), key.flatten(0, 1),
119 maximum, total, indexer_stats, scale=module.scaling,
120 query_rope=query_rope.flatten(0, 1), key_rope=key_rope.flatten(0, 1),
121 query_lengths=query_lengths, key_lengths=key_lengths, loss_coefficient=module.dsa_loss_coeff,
122 )
123 output = aux_loss_auto_scale(output, loss)
124 return output
127def apply_mome(
128 hidden_states: torch.Tensor,
129 mome_mask: torch.Tensor | None,
130 convolution: nn.Conv1d,
131 *,
132 fused: bool,
133) -> torch.Tensor:
134 """Apply masked causal depthwise convolution and residual."""
135 if mome_mask is None:
136 raise ValueError("mome_mask is required when MOME is enabled")
137 if mome_mask.shape != hidden_states.shape[:2]:
138 raise ValueError(
139 f"mome_mask must have shape {hidden_states.shape[:2]}, "
140 f"but got {tuple(mome_mask.shape)}"
141 )
143 mome_mask = mome_mask.to(device=hidden_states.device, dtype=torch.bool)
144 padding = convolution.kernel_size[0] - 1
145 padded_states = F.pad(hidden_states, (0, 0, padding, 0))
146 if fused:
147 padded_mask = F.pad(mome_mask, (padding, 0), value=False)
148 weight = convolution.weight.squeeze(1).transpose(0, 1)
149 mixed_states = aggregate_hidden( # pylint: disable=not-callable
150 padded_states.transpose(0, 1).contiguous(),
151 weight,
152 padded_mask,
153 )
154 mixed_states = mixed_states[padding:].transpose(0, 1).contiguous()
155 else:
156 mixed_states = convolution(padded_states.transpose(1, 2)).transpose(1, 2)
157 mixed_states = mixed_states * mome_mask.unsqueeze(-1).to(mixed_states.dtype)
158 return hidden_states + mixed_states
161def _apply_attention_rope(
162 query: torch.Tensor,
163 key: torch.Tensor,
164 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None,
165 *,
166 interleaved: bool,
167) -> tuple[torch.Tensor, torch.Tensor]:
168 """Apply the configured RoPE convention to an attention query/key pair."""
169 if position_embeddings is None:
170 return query, key
171 cos, sin = position_embeddings
172 rotary_op = apply_rotary_pos_emb_interleave if interleaved else apply_rotary_pos_emb
173 return rotary_op(query, key, cos, sin, unsqueeze_dim=2)
176def _apply_index_rope(
177 query: torch.Tensor,
178 key: torch.Tensor,
179 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None,
180 *,
181 rope_head_dim: int,
182 index_head_dim: int,
183 interleaved: bool,
184) -> tuple[torch.Tensor, torch.Tensor]:
185 """Apply RoPE to the rotary portion of an indexer query/key pair."""
186 if position_embeddings is None:
187 return query, key
188 cos, sin = position_embeddings
189 split_sizes = (rope_head_dim, index_head_dim - rope_head_dim)
190 query_rot, query_pass = torch.split(query, split_sizes, dim=-1)
191 key_rot, key_pass = torch.split(key, split_sizes, dim=-1)
192 rotary_op = apply_rotary_pos_emb_interleave if interleaved else apply_rotary_pos_emb
193 query_rot, key_rot = rotary_op(query_rot, key_rot, cos, sin, unsqueeze_dim=2)
194 return torch.cat((query_rot, query_pass), dim=-1), torch.cat((key_rot, key_pass), dim=-1)
197def _absorb_query_projection(
198 query: torch.Tensor,
199 key_up_weight: torch.Tensor,
200 *,
201 num_heads: int,
202 batch_size: int,
203 seq_length: int,
204 input_dim: int,
205 output_dim: int,
206) -> torch.Tensor:
207 """Absorb the key up-projection weight into query states."""
208 query = query.permute(2, 0, 1, 3).reshape(num_heads, batch_size * seq_length, input_dim)
209 query = torch.bmm(query, key_up_weight).view(num_heads, batch_size, seq_length, output_dim)
210 return query.permute(1, 2, 0, 3)
213def _restore_attention_projection(
214 output: torch.Tensor,
215 value_up_weight: torch.Tensor,
216 *,
217 num_heads: int,
218 batch_size: int,
219 seq_length: int,
220 kv_lora_rank: int,
221 value_head_dim: int,
222) -> torch.Tensor:
223 """Apply the absorbed value up-projection and restore the hidden layout."""
224 output = output[..., :kv_lora_rank]
225 output = output.permute(2, 0, 1, 3).reshape(num_heads, batch_size * seq_length, kv_lora_rank)
226 output = torch.bmm(output, value_up_weight)
227 return output.view(num_heads, batch_size, seq_length, value_head_dim).permute(1, 2, 0, 3).reshape(
228 batch_size, seq_length, -1
229 )
232@module_replacement
233class DeepseekV32DSAAttention(nn.Module):
234 """NPU DSA replacement for Transformers ``DeepseekV32Attention``.
236 It supports causal training and packed sequences. Packed boundaries must
237 be supplied through ``actual_seq_len``. The optional ``attention_mask`` is
238 assumed to be the standard causal mask created by Transformers; padding,
239 custom attention masks, dropout, and KV cache are not supported.
240 """
242 @staticmethod
243 def _validate_source(module: nn.Module) -> None:
244 """Validate the required DSA module and indexer surface."""
245 required = (
246 "q_a_proj", "q_a_layernorm", "q_b_proj", "kv_a_proj_with_mqa",
247 "kv_a_layernorm", "kv_b_proj", "o_proj", "indexer",
248 )
249 missing = [name for name in required if not hasattr(module, name)]
250 if missing:
251 raise TypeError(f"DeepseekV32DSAAttention source module is missing: {missing}")
252 indexer_required = ("wq_b", "wk", "k_norm", "weights_proj")
253 indexer_missing = [name for name in indexer_required if not hasattr(module.indexer, name)]
254 if indexer_missing:
255 raise TypeError(f"DeepseekV32DSAAttention source indexer is missing: {indexer_missing}")
257 def _initialize_dimensions(self, module: nn.Module) -> None:
258 """Copy main-attention and indexer dimensions from the source module."""
259 config = self.config
260 self.layer_idx = getattr(module, "layer_idx", getattr(module, "layer_number", None))
261 self.num_heads = getattr(module, "num_heads", config.num_attention_heads)
262 self.q_lora_rank = getattr(module, "q_lora_rank", config.q_lora_rank)
263 self.kv_lora_rank = getattr(module, "kv_lora_rank", config.kv_lora_rank)
264 self.qk_rope_head_dim = getattr(module, "qk_rope_head_dim", config.qk_rope_head_dim)
265 self.qk_nope_head_dim = getattr(module, "qk_nope_head_dim", config.qk_nope_head_dim)
266 self.v_head_dim = getattr(module, "v_head_dim", config.v_head_dim)
267 self.qk_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
268 if self.q_lora_rank is None:
269 raise ValueError("DeepseekV32DSAAttention requires the Q-LoRA projection path")
270 self.index_head_dim = getattr(module.indexer, "head_dim", config.index_head_dim)
271 self.num_index_heads = getattr(
272 module.indexer,
273 "n_heads",
274 getattr(config, "index_n_heads", getattr(config, "index_num_attention_heads", None)),
275 )
276 if self.num_index_heads is None:
277 raise ValueError("DeepseekV32DSAAttention requires the number of indexer heads")
278 self.index_topk = getattr(module.indexer, "index_topk", config.index_topk)
280 def _initialize_options(self, module: nn.Module) -> None:
281 """Copy supported DSA options and reject unsupported source modes."""
282 config = self.config
283 self.scaling = getattr(module, "scaling", self.qk_head_dim**-0.5)
284 attention_dropout = getattr(module, "attention_dropout", getattr(config, "attention_dropout", 0.0))
285 if isinstance(attention_dropout, nn.Dropout):
286 attention_dropout = attention_dropout.p
287 if attention_dropout != 0.0:
288 raise ValueError(
289 "DeepseekV32DSAAttention requires attention_dropout=0 because "
290 "the sparse-attention operator does not apply dropout"
291 )
292 source_rotary_interleaved = getattr(module, "rotary_interleaved", None)
293 if source_rotary_interleaved is None:
294 source_rotary_interleaved = getattr(config, "rope_interleave", None)
295 if source_rotary_interleaved is None:
296 source_rotary_interleaved = getattr(config, "rope_interleaved", None)
297 self.rotary_interleaved = bool(True if source_rotary_interleaved is None else source_rotary_interleaved)
298 self.index_rotary_interleaved = bool(
299 getattr(module.indexer, "rotary_interleaved", getattr(config, "index_rope_interleaved", False))
300 )
301 self.dsa_loss_coeff = getattr(config, "dsa_loss_coeff", 0.0)
302 self.freeze_dsa = bool(getattr(config, "freeze_DSA", getattr(config, "freeze_dsa", False)))
303 self.dsa_dense_warm_up = bool(
304 getattr(module, "dsa_dense_warm_up", getattr(config, "dsa_dense_warm_up", False))
305 )
306 if getattr(module, "param_sink_number", 0) > 0:
307 raise ValueError("DeepseekV32DSAAttention does not support the parameter-sink path")
308 if getattr(module, "use_mome", False):
309 raise ValueError("DeepseekV32DSAAttention does not support the MOME path")
311 def _initialize_source_layers(self, module: nn.Module) -> None:
312 """Fuse latent projections and attach the remaining source layers."""
313 q_a = module.q_a_proj
314 kv_a = module.kv_a_proj_with_mqa
315 self._q_latent_output_size = q_a.out_features
316 self._kv_latent_output_size = kv_a.out_features
317 can_fuse = (
318 q_a.in_features == kv_a.in_features
319 and q_a.weight.requires_grad == kv_a.weight.requires_grad
320 and (q_a.bias is None) == (kv_a.bias is None)
321 and (q_a.bias is None or q_a.bias.requires_grad == kv_a.bias.requires_grad)
322 )
323 if not can_fuse:
324 raise ValueError("Q and KV latent projections cannot be represented by one fused projection")
325 self.linear_qkv = nn.Linear(
326 q_a.in_features,
327 q_a.out_features + kv_a.out_features,
328 bias=q_a.bias is not None,
329 device=q_a.weight.device,
330 dtype=q_a.weight.dtype,
331 )
332 self.linear_qkv.weight.requires_grad_(q_a.weight.requires_grad)
333 if q_a.bias is not None:
334 self.linear_qkv.bias.requires_grad_(q_a.bias.requires_grad)
335 for name in ("q_a_layernorm", "kv_a_layernorm", "q_b_proj", "kv_b_proj", "o_proj", "indexer"):
336 setattr(self, name, getattr(module, name))
338 def __init__(
339 self,
340 *,
341 module: nn.Module,
342 module_fqn: str = "",
343 context: Mapping[str, Any] | None = None,
344 ) -> None:
345 """Build the high-performance module from a Transformers DSA module."""
346 super().__init__()
347 del module_fqn, context
348 self._validate_source(module)
349 config = module.config
350 self.config = config
351 if hasattr(config, "indexer_types"):
352 raise TypeError(
353 "DeepseekV32DSAAttention supports the DeepSeek-V3.2 DSA contract; "
354 "GLM-MOE-DSA shared indexers and three-value forward result require "
355 "a dedicated replacement"
356 )
357 self._initialize_dimensions(module)
358 self._initialize_options(module)
359 self._initialize_source_layers(module)
360 self.train(module.training)
362 def make_transforms(self) -> list[WeightConverter]:
363 """Describe reversible source-checkpoint to high-performance conversion."""
364 transforms: list[WeightConverter] = [
365 WeightConverter(
366 source_patterns=[
367 "q_a_proj.weight",
368 "kv_a_proj_with_mqa.weight",
369 ],
370 target_patterns="linear_qkv.weight",
371 operations=[
372 ConcatenateWithSections(
373 sections=(
374 self._q_latent_output_size,
375 self._kv_latent_output_size,
376 ),
377 dim=0,
378 )
379 ],
380 ),
381 ]
382 if self.linear_qkv.bias is not None:
383 transforms.insert(
384 1,
385 WeightConverter(
386 source_patterns=["q_a_proj.bias", "kv_a_proj_with_mqa.bias"],
387 target_patterns="linear_qkv.bias",
388 operations=[
389 ConcatenateWithSections(
390 sections=(
391 self._q_latent_output_size,
392 self._kv_latent_output_size,
393 ),
394 dim=0,
395 )
396 ],
397 ),
398 )
399 return transforms
401 @staticmethod
402 def _get_actual_seq_len(
403 actual_seq_len: torch.Tensor | Sequence[int] | None,
404 batch_size: int,
405 seq_length: int,
406 device: torch.device,
407 ) -> torch.Tensor:
408 """Normalize cumulative sequence lengths for the TND custom operators."""
409 if isinstance(actual_seq_len, torch.Tensor):
410 return actual_seq_len.to(device=device, dtype=torch.int32)
411 if actual_seq_len is not None:
412 return torch.tensor(actual_seq_len, device=device, dtype=torch.int32)
413 return torch.arange(
414 seq_length,
415 (batch_size + 1) * seq_length,
416 seq_length,
417 dtype=torch.int32,
418 device=device,
419 )
421 @staticmethod
422 def _invalid_position_ids(
423 position_ids: torch.Tensor | None,
424 batch_size: int,
425 seq_length: int,
426 ) -> bool:
427 """Return whether position IDs violate the supported dense layout."""
428 if position_ids is None:
429 return False
430 if position_ids.ndim != 2:
431 return True
432 return position_ids.shape[0] not in (1, batch_size) or position_ids.shape[1] != seq_length
434 @staticmethod
435 def _validate_forward_inputs(
436 hidden_states: torch.Tensor,
437 attention_mask: torch.Tensor | None,
438 past_key_values: Any | None,
439 position_ids: torch.Tensor | None,
440 kwargs: dict[str, Any],
441 ) -> None:
442 """Validate inputs supported by the DeepSeek sparse-attention path."""
443 batch_size, seq_length = hidden_states.shape[:-1]
444 if past_key_values is not None:
445 raise NotImplementedError("DeepseekV32DSAAttention does not support KV cache")
446 if DeepseekV32DSAAttention._invalid_position_ids(position_ids, batch_size, seq_length):
447 raise ValueError("position_ids must have shape [1 or batch_size, sequence_length]")
448 if attention_mask is not None:
449 expected_shape = (batch_size, 1, seq_length, seq_length)
450 if tuple(attention_mask.shape) != expected_shape:
451 raise ValueError(
452 "DeepseekV32DSAAttention supports only a Transformers 4D causal mask; "
453 "padding and custom masks are not supported"
454 )
455 if kwargs.pop("output_attentions", False):
456 raise NotImplementedError("DeepseekV32DSAAttention does not expose attention weights")
458 def _project_attention_states(
459 self,
460 hidden_states: torch.Tensor,
461 *,
462 use_dense: bool = False,
463 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
464 """Project MLA Q/KV, retaining Q before absorption during dense warm-up."""
465 batch_size, seq_length = hidden_states.shape[:-1]
466 latent_states = self.linear_qkv(hidden_states)
467 q_latent, kv_nope, k_rot = torch.split(
468 latent_states, (self.q_lora_rank, self.kv_lora_rank, self.qk_rope_head_dim), dim=-1
469 )
470 q_resid = self.q_a_layernorm(q_latent)
471 q_states = self.q_b_proj(q_resid).view(batch_size, seq_length, self.num_heads, self.qk_head_dim)
472 q_pass, q_rot = torch.split(q_states, (self.qk_nope_head_dim, self.qk_rope_head_dim), dim=-1)
473 kv_weight = self.kv_b_proj.weight.view(
474 self.num_heads, self.qk_nope_head_dim + self.v_head_dim, self.kv_lora_rank
475 )
476 absorbed_query = q_pass if use_dense else _absorb_query_projection(
477 q_pass,
478 kv_weight[:, : self.qk_nope_head_dim],
479 num_heads=self.num_heads,
480 batch_size=batch_size,
481 seq_length=seq_length,
482 input_dim=self.qk_nope_head_dim,
483 output_dim=self.kv_lora_rank,
484 )
485 kv_nope = self.kv_a_layernorm(kv_nope).view(batch_size, seq_length, 1, self.kv_lora_rank)
486 k_rot = k_rot.view(batch_size, seq_length, 1, self.qk_rope_head_dim)
487 return q_resid, absorbed_query, kv_nope, q_rot, k_rot, kv_weight
489 def _project_index_states(
490 self,
491 hidden_states: torch.Tensor,
492 q_resid: torch.Tensor,
493 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None,
494 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
495 """Build indexer query, key, and merge-weight states."""
496 batch_size, seq_length = hidden_states.shape[:-1]
497 index_query = self.indexer.wq_b(q_resid.detach()).view(
498 batch_size, seq_length, self.num_index_heads, self.index_head_dim
499 )
500 index_key = self.indexer.k_norm(self.indexer.wk(hidden_states.detach())).unsqueeze(2)
501 merge_weight = self.indexer.weights_proj(hidden_states.detach())
502 merge_weight = merge_weight * self.num_index_heads**-0.5 * self.index_head_dim**-0.5
503 index_query, index_key = _apply_index_rope(
504 index_query,
505 index_key,
506 position_embeddings,
507 rope_head_dim=self.qk_rope_head_dim,
508 index_head_dim=self.index_head_dim,
509 interleaved=self.index_rotary_interleaved,
510 )
511 return index_query, index_key, merge_weight
513 def _apply_auxiliary_loss(
514 self,
515 attn_output: torch.Tensor,
516 index_states: tuple[torch.Tensor, torch.Tensor, torch.Tensor],
517 attention_states: tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor],
518 topk_indices: torch.Tensor,
519 softmax_stats: tuple[torch.Tensor, torch.Tensor],
520 actual_q_len: torch.Tensor,
521 actual_kv_len: torch.Tensor,
522 ) -> torch.Tensor:
523 """Attach the DSA KL auxiliary loss when training enables it."""
524 if not (self.training and not self.freeze_dsa and self.dsa_loss_coeff):
525 return attn_output
526 query_tnd, key_tnd, q_rot_tnd, k_rot_tnd = (
527 tensor.reshape(-1, tensor.shape[2], tensor.shape[3])
528 for tensor in attention_states
529 )
530 aux_loss = dsa_kl_loss(
531 *index_states, query_tnd, key_tnd,
532 topk_indices, *softmax_stats, q_rot_tnd, k_rot_tnd,
533 actual_q_len, actual_kv_len, self.scaling, self.dsa_loss_coeff,
534 backend="cann",
535 )
536 return aux_loss_auto_scale(attn_output, aux_loss)
538 def _prepare_attention_states(self, hidden_states, position_embeddings):
539 """Project attention states and apply their rotary embeddings."""
540 q_resid, absorbed_query_states, kv_nope, q_rot, k_rot, kv_weight = self._project_attention_states(
541 hidden_states, use_dense=self.dsa_dense_warm_up,
542 )
543 q_rot, k_rot = _apply_attention_rope(
544 q_rot, k_rot, position_embeddings, interleaved=self.rotary_interleaved
545 )
546 return q_resid, (absorbed_query_states, kv_nope, q_rot, k_rot), kv_weight
548 def _run_indexer(self, hidden_states, q_resid, position_embeddings, actual_seq_len, kwargs):
549 """Resolve packed boundaries and select sparse attention indices."""
550 index_states = self._project_index_states(hidden_states, q_resid, position_embeddings)
551 packed_kwargs = dict(kwargs)
552 packed_kwargs["actual_seq_len"] = actual_seq_len
553 actual_q_len, actual_kv_len = resolve_packed_sequence_lengths(
554 packed_kwargs,
555 hidden_states.shape[0] * hidden_states.shape[1],
556 hidden_states.shape[0] * hidden_states.shape[1],
557 )
558 actual_q_len = self._get_actual_seq_len(
559 actual_q_len, *hidden_states.shape[:-1], hidden_states.device,
560 )
561 actual_kv_len = self._get_actual_seq_len(
562 actual_kv_len, *hidden_states.shape[:-1], hidden_states.device,
563 )
564 indexed = dsa_indexer(
565 *index_states, actual_q_len, actual_kv_len, self.index_topk, backend="cann",
566 )
567 return indexed[0], indexed[1:], actual_q_len, actual_kv_len
569 def _compute_sparse_attention(
570 self, hidden_states, q_resid, attention_states, position_embeddings, actual_seq_len, kwargs,
571 ):
572 """Run sparse attention and attach its optional auxiliary loss."""
573 topk_indices, index_states, actual_q_len, actual_kv_len = self._run_indexer(
574 hidden_states, q_resid, position_embeddings, actual_seq_len, kwargs,
575 )
576 attn_output, softmax_max, softmax_sum = dsa_sparse_attention(
577 *attention_states, topk_indices, self.scaling, actual_q_len, actual_kv_len,
578 backend="cann",
579 )
580 return self._apply_auxiliary_loss(
581 attn_output, index_states, attention_states, topk_indices,
582 (softmax_max, softmax_sum), actual_q_len, actual_kv_len,
583 )
585 def _compute_attention_output(
586 self, hidden_states, position_embeddings, actual_seq_len, kwargs,
587 ):
588 """Run the selected dense/sparse stage and restore the value projection."""
589 batch_size, seq_length = hidden_states.shape[:-1]
590 q_resid, attention_states, kv_weight = self._prepare_attention_states(
591 hidden_states, position_embeddings,
592 )
593 if self.dsa_dense_warm_up:
594 index_states = self._project_index_states(hidden_states, q_resid, position_embeddings)
595 packed_kwargs = {**kwargs, "actual_seq_len": actual_seq_len}
596 lengths = resolve_packed_sequence_lengths(
597 packed_kwargs, batch_size * seq_length, batch_size * seq_length,
598 )
599 lengths = tuple(self._get_actual_seq_len(
600 value, batch_size, seq_length, hidden_states.device,
601 ) for value in lengths)
602 output = _deepseek_v32_dense_attention(self, index_states, attention_states, lengths)
603 return output.flatten(2)
604 attn_output = self._compute_sparse_attention(
605 hidden_states, q_resid, attention_states, position_embeddings, actual_seq_len, kwargs,
606 )
607 value_up_weight = kv_weight[:, self.qk_nope_head_dim :].transpose(1, 2)
608 return _restore_attention_projection(
609 attn_output,
610 value_up_weight,
611 num_heads=self.num_heads,
612 batch_size=batch_size,
613 seq_length=seq_length,
614 kv_lora_rank=self.kv_lora_rank,
615 value_head_dim=self.v_head_dim,
616 )
618 def forward(
619 self,
620 hidden_states: torch.Tensor,
621 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
622 attention_mask: torch.Tensor | None = None,
623 past_key_values: Any | None = None,
624 position_ids: torch.Tensor | None = None,
625 actual_seq_len: torch.Tensor | Sequence[int] | None = None,
626 **kwargs: Any,
627 ) -> tuple[torch.Tensor, None]:
628 """Run causal or packed DSA with the NPU sparse-attention kernels."""
629 self._validate_forward_inputs(hidden_states, attention_mask, past_key_values, position_ids, kwargs)
630 attn_output = self._compute_attention_output(
631 hidden_states, position_embeddings, actual_seq_len, kwargs,
632 )
633 attn_output = self.o_proj(attn_output)
634 return attn_output, None
637@module_replacement
638class DSAAttention(nn.Module):
639 """NPU-optimized replacement for a DSA attention module.
641 Packed boundaries are supplied through ``actual_seq_len``. Attention masks,
642 KV reuse, and KV cache are not supported. The source parameter layout is
643 preserved, so no checkpoint conversion is required.
644 """
646 def __init__(
647 self,
648 *,
649 module: nn.Module,
650 module_fqn: str = "",
651 context: Mapping[str, Any] | None = None,
652 ) -> None:
653 """Build the replacement from a compatible DSA attention module."""
654 super().__init__()
655 del module_fqn, context
656 required = (
657 "linear_qkv",
658 "q_layernorm",
659 "k_layernorm",
660 "linear_qb",
661 "linear_kvb",
662 "linear_proj",
663 "index_linear_qb",
664 "index_linear_k",
665 "index_k_layernorm",
666 "linear_merge_weight",
667 )
668 missing = [name for name in required if not hasattr(module, name)]
669 if missing:
670 raise TypeError(f"DSAAttention source module is missing: {missing}")
672 unsupported = {
673 "mla_mm_split": bool(getattr(module, "mla_mm_split", False)),
674 "dense warm-up": bool(getattr(module, "dsa_dense_warm_up", False)),
675 }
676 enabled = [name for name, value in unsupported.items() if value]
677 if enabled:
678 raise NotImplementedError(
679 f"DSAAttention does not yet support: {', '.join(enabled)}"
680 )
682 self.config = module.config
683 self.layer_number = module.layer_number
684 self.num_heads = module.num_heads
685 self.q_lora_rank = module.q_lora_rank
686 self.kv_lora_rank = module.kv_lora_rank
687 self.qk_rope_head_dim = module.qk_rope_head_dim
688 self.qk_nope_head_dim = module.qk_nope_head_dim
689 self.qk_head_dim = module.qk_head_dim
690 self.v_head_dim = module.v_head_dim
691 self.index_head_dim = module.index_head_dim
692 self.num_index_heads = module.num_index_heads
693 self.index_topk = module.index_topk
694 self.rotary_interleaved = module.rotary_interleaved
695 self.dsa_loss_coeff = module.dsa_loss_coeff
696 self.freeze_dsa = module.freeze_dsa
697 self.use_flash_attn = bool(getattr(module, "use_flash_attn", True))
698 self.use_mome = bool(getattr(module, "use_mome", False))
699 self.use_fused_mome = bool(getattr(module, "use_fused_mome", False))
700 self.param_sink_number = int(getattr(module, "param_sink_number", 0))
701 self.param_sink_scalar = getattr(module, "param_sink_scalar", None)
702 self.apply_fa_rescale = bool(
703 getattr(module, "apply_fa_rescale", False)
704 )
705 self.attention_dropout = getattr(module, "attention_dropout", nn.Dropout(0.0))
706 if self.param_sink_number > 0:
707 if self.param_sink_scalar:
708 raise NotImplementedError(
709 "DSAAttention does not support scalar parameter sink"
710 )
711 if not self.apply_fa_rescale:
712 raise NotImplementedError(
713 "DSAAttention supports parameter sink through "
714 "apply_fa_rescale only"
715 )
717 self.linear_qkv = module.linear_qkv
718 self.q_layernorm = module.q_layernorm
719 self.k_layernorm = module.k_layernorm
720 self.linear_qb = module.linear_qb
721 self.linear_kvb = module.linear_kvb
722 self.linear_proj = module.linear_proj
723 self.index_linear_qb = module.index_linear_qb
724 self.index_linear_k = module.index_linear_k
725 self.index_k_layernorm = module.index_k_layernorm
726 self.linear_merge_weight = module.linear_merge_weight
727 if self.use_mome:
728 self.qa_conv = module.qa_conv
729 self.compresskv_conv = module.compresskv_conv
730 self.o_conv = module.o_conv
731 if self.param_sink_number > 0:
732 self.param_sink_k_pe = module.param_sink_k_pe
733 self.param_sink_compressed_kv = module.param_sink_compressed_kv
734 self.train(module.training)
736 @staticmethod
737 def _get_actual_seq_len(
738 actual_seq_len: torch.Tensor | Sequence[int] | None,
739 batch_size: int,
740 seq_length: int,
741 device: torch.device,
742 ) -> torch.Tensor:
743 """Normalize cumulative sequence lengths for the TND custom operators."""
744 if actual_seq_len is None:
745 return torch.arange(
746 seq_length,
747 (batch_size + 1) * seq_length,
748 seq_length,
749 dtype=torch.int32,
750 device=device,
751 )
752 return torch.as_tensor(actual_seq_len, dtype=torch.int32, device=device)
754 def _prepare_param_sink(
755 self, batch_size: int
756 ) -> tuple[torch.Tensor, torch.Tensor]:
757 """Construct the DSA sink key and value in BSND layout."""
758 sink_rotary_key = self.param_sink_k_pe.unsqueeze(0).unsqueeze(2).expand(
759 batch_size, -1, -1, -1
760 )
761 sink_latent = self.param_sink_compressed_kv.unsqueeze(0).unsqueeze(2).expand(
762 batch_size, -1, -1, -1
763 )
764 sink_latent = self.k_layernorm(sink_latent)
765 sink_value = (
766 F.pad(sink_latent, (0, self.qk_rope_head_dim))
767 if self.use_flash_attn and self.qk_rope_head_dim > 0
768 else sink_latent
769 )
770 sink_key = torch.cat((sink_latent, sink_rotary_key), dim=-1)
771 return sink_key, sink_value
773 @staticmethod
774 def _validate_forward_inputs(
775 attention_mask: torch.Tensor | None,
776 kv_reuse_states: Any | None,
777 past_key_values: Any | None,
778 cache_position: torch.Tensor | None,
779 output_attentions: bool,
780 ) -> None:
781 """Validate inputs supported by the optimized DSA path."""
782 if attention_mask is not None:
783 raise ValueError(
784 "DSAAttention does not consume attention_mask; pass packed sequence "
785 "boundaries through actual_seq_len"
786 )
787 if kv_reuse_states is not None:
788 raise NotImplementedError("DSAAttention does not support KV reuse")
789 if past_key_values is not None or cache_position is not None:
790 raise NotImplementedError("DSAAttention does not support KV cache")
791 if output_attentions:
792 raise NotImplementedError("DSAAttention does not expose attention weights")
794 def _project_attention_states(
795 self,
796 hidden_states: torch.Tensor,
797 mome_mask: torch.Tensor | None,
798 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
799 """Project hidden states into absorbed-query and latent KV states."""
800 batch_size, seq_length = hidden_states.shape[:-1]
801 latent_states, _ = self.linear_qkv(hidden_states)
802 q_latent, kv_nope, k_rot = torch.split(
803 latent_states, (self.q_lora_rank, self.kv_lora_rank, self.qk_rope_head_dim), dim=-1
804 )
805 if self.use_mome:
806 q_latent = apply_mome(q_latent, mome_mask, self.qa_conv, fused=False)
807 kv_nope = apply_mome(kv_nope, mome_mask, self.compresskv_conv, fused=False)
808 q_resid = self.q_layernorm(q_latent)
809 q_states, _ = self.linear_qb(q_resid.contiguous())
810 q_states = q_states.view(batch_size, seq_length, self.num_heads, self.qk_head_dim)
811 q_pass, q_rot = torch.split(q_states, (self.qk_nope_head_dim, self.qk_rope_head_dim), dim=-1)
812 kv_weight = self.linear_kvb.weight.view(
813 self.num_heads, self.qk_nope_head_dim + self.v_head_dim, self.kv_lora_rank
814 )
815 absorbed_query = _absorb_query_projection(
816 q_pass,
817 kv_weight[:, : self.qk_nope_head_dim],
818 num_heads=self.num_heads,
819 batch_size=batch_size,
820 seq_length=seq_length,
821 input_dim=self.qk_nope_head_dim,
822 output_dim=self.kv_lora_rank,
823 )
824 kv_nope = self.k_layernorm(kv_nope).view(batch_size, seq_length, 1, self.kv_lora_rank)
825 k_rot = k_rot.view(batch_size, seq_length, 1, self.qk_rope_head_dim)
826 return q_resid, absorbed_query, kv_nope, q_rot, k_rot, kv_weight
828 def _project_index_states(
829 self,
830 hidden_states: torch.Tensor,
831 q_resid: torch.Tensor,
832 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None,
833 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
834 """Build indexer states and apply their rotary embedding."""
835 batch_size, seq_length = hidden_states.shape[:-1]
836 index_query, _ = self.index_linear_qb(q_resid.detach())
837 index_query = index_query.view(batch_size, seq_length, self.num_index_heads, self.index_head_dim)
838 index_key, _ = self.index_linear_k(hidden_states.detach())
839 index_key = self.index_k_layernorm(index_key.unsqueeze(2))
840 merge_weight, _ = self.linear_merge_weight(hidden_states.detach())
841 merge_weight = merge_weight * self.num_index_heads**-0.5 * self.index_head_dim**-0.5
842 index_query, index_key = _apply_index_rope(
843 index_query,
844 index_key,
845 position_embeddings,
846 rope_head_dim=self.qk_rope_head_dim,
847 index_head_dim=self.index_head_dim,
848 interleaved=self.rotary_interleaved,
849 )
850 return index_query, index_key, merge_weight
852 def _run_sparse_attention(
853 self,
854 attention_states: tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor],
855 topk_indices: torch.Tensor,
856 actual_q_len: torch.Tensor,
857 actual_kv_len: torch.Tensor,
858 batch_size: int,
859 seq_length: int,
860 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
861 """Run sparse attention with or without parameter-sink rescaling."""
862 absorbed_query, kv_nope, q_rot, k_rot = attention_states
863 sparse_scale = self.qk_head_dim**-0.5
864 if self.param_sink_number <= 0:
865 return dsa_sparse_attention( # pylint: disable=not-callable
866 absorbed_query, kv_nope, q_rot, k_rot, topk_indices,
867 sparse_scale, actual_q_len, actual_kv_len,
868 )
869 sink_key, sink_value = self._prepare_param_sink(batch_size)
870 return dsa_sparse_attention_rescale( # pylint: disable=not-callable
871 absorbed_query, kv_nope, q_rot, k_rot, sink_key, sink_value,
872 topk_indices, batch_size, seq_length, self.num_heads, sparse_scale,
873 1 - self.attention_dropout.p, actual_q_len, actual_kv_len,
874 )
876 def _apply_auxiliary_loss(
877 self,
878 attn_output: torch.Tensor,
879 index_states: tuple[torch.Tensor, torch.Tensor, torch.Tensor],
880 attention_states: tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor],
881 topk_indices: torch.Tensor,
882 softmax_stats: tuple[torch.Tensor, torch.Tensor],
883 actual_q_len: torch.Tensor,
884 actual_kv_len: torch.Tensor,
885 ) -> torch.Tensor:
886 """Attach the DSA KL auxiliary loss when training enables it."""
887 if not (self.training and not self.freeze_dsa and self.dsa_loss_coeff):
888 return attn_output
889 query_tnd, key_tnd, q_rot_tnd, k_rot_tnd = (
890 tensor.reshape(-1, tensor.shape[2], tensor.shape[3])
891 for tensor in attention_states
892 )
893 aux_loss = dsa_kl_loss( # pylint: disable=not-callable
894 *index_states, query_tnd, key_tnd,
895 topk_indices, *softmax_stats, q_rot_tnd, k_rot_tnd,
896 actual_q_len, actual_kv_len, self.qk_head_dim**-0.5, self.dsa_loss_coeff,
897 )
898 return aux_loss_auto_scale(attn_output, aux_loss)
900 def _prepare_attention_states(self, hidden_states, position_embeddings, mome_mask):
901 """Project attention states and apply their rotary embeddings."""
902 q_resid, absorbed_query_states, kv_nope, q_rot, k_rot, kv_weight = self._project_attention_states(
903 hidden_states, mome_mask
904 )
905 q_rot, k_rot = _apply_attention_rope(
906 q_rot, k_rot, position_embeddings, interleaved=self.rotary_interleaved
907 )
908 return q_resid, (absorbed_query_states, kv_nope, q_rot, k_rot), kv_weight
910 def _run_indexer(self, hidden_states, q_resid, position_embeddings, actual_seq_len, kwargs):
911 """Resolve packed boundaries and select sparse attention indices."""
912 index_states = self._project_index_states(hidden_states, q_resid, position_embeddings)
913 packed_kwargs = dict(kwargs)
914 packed_kwargs["actual_seq_len"] = actual_seq_len
915 actual_q_len, actual_kv_len = resolve_packed_sequence_lengths(
916 packed_kwargs,
917 hidden_states.shape[0] * hidden_states.shape[1],
918 hidden_states.shape[0] * hidden_states.shape[1],
919 )
920 actual_q_len = self._get_actual_seq_len(
921 actual_q_len, *hidden_states.shape[:-1], hidden_states.device,
922 )
923 actual_kv_len = self._get_actual_seq_len(
924 actual_kv_len, *hidden_states.shape[:-1], hidden_states.device,
925 )
926 indexed = dsa_indexer( # pylint: disable=not-callable
927 *index_states, actual_q_len, actual_kv_len, self.index_topk
928 )
929 return indexed[0], indexed[1:], actual_q_len, actual_kv_len
931 def _compute_sparse_attention(
932 self, hidden_states, q_resid, attention_states, position_embeddings, actual_seq_len, kwargs,
933 ):
934 """Run sparse attention and attach its optional auxiliary loss."""
935 topk_indices, index_states, actual_q_len, actual_kv_len = self._run_indexer(
936 hidden_states, q_resid, position_embeddings, actual_seq_len, kwargs,
937 )
938 attn_output, softmax_max, softmax_sum = self._run_sparse_attention(
939 attention_states, topk_indices, actual_q_len, actual_kv_len, *hidden_states.shape[:-1],
940 )
941 return self._apply_auxiliary_loss(
942 attn_output, index_states, attention_states, topk_indices,
943 (softmax_max, softmax_sum), actual_q_len, actual_kv_len,
944 )
946 def _compute_attention_output(
947 self, hidden_states, position_embeddings, actual_seq_len, kwargs, mome_mask,
948 ):
949 """Compute sparse attention and restore its value projection."""
950 batch_size, seq_length = hidden_states.shape[:-1]
951 q_resid, attention_states, kv_weight = self._prepare_attention_states(
952 hidden_states, position_embeddings, mome_mask,
953 )
954 attn_output = self._compute_sparse_attention(
955 hidden_states, q_resid, attention_states, position_embeddings, actual_seq_len, kwargs,
956 )
957 value_up_weight = kv_weight[:, self.qk_nope_head_dim :].transpose(1, 2)
958 return _restore_attention_projection(
959 attn_output,
960 value_up_weight,
961 num_heads=self.num_heads,
962 batch_size=batch_size,
963 seq_length=seq_length,
964 kv_lora_rank=self.kv_lora_rank,
965 value_head_dim=self.v_head_dim,
966 )
968 def forward(
969 self,
970 hidden_states: torch.Tensor,
971 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
972 attention_mask: torch.Tensor | None = None,
973 past_key_values: Any | None = None,
974 cache_position: torch.Tensor | None = None,
975 actual_seq_len: torch.Tensor | Sequence[int] | None = None,
976 kv_reuse_states: Any | None = None,
977 output_attentions: bool = False,
978 return_bias: bool = False,
979 mome_mask: torch.Tensor | None = None,
980 **kwargs: Any,
981 ) -> tuple[torch.Tensor, torch.Tensor | None]:
982 """Run DSA with its configured MOME and parameter-sink paths."""
983 self._validate_forward_inputs(
984 attention_mask, kv_reuse_states, past_key_values, cache_position, output_attentions
985 )
986 attn_output = self._compute_attention_output(
987 hidden_states, position_embeddings, actual_seq_len, kwargs, mome_mask,
988 )
989 if self.use_mome:
990 attn_output = apply_mome(
991 attn_output,
992 mome_mask,
993 self.o_conv,
994 fused=self.use_fused_mome,
995 )
996 output, bias = self.linear_proj(attn_output)
997 return (output, bias) if return_bias else (output, None)