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"""Dense MLA Ulysses with expanded QKV or shared latent-KV communication."""
16
17from __future__ import annotations
18
19from collections.abc import Callable, Sequence
20from dataclasses import dataclass
21from functools import lru_cache, wraps
22import math
23from typing import Any, Literal
24
25# Native Torch is required, as in the existing CP adapters; the legacy lint rule predates that migration.
26import torch # pylint: disable=forbidden-backend-import
27from torch import nn # pylint: disable=forbidden-backend-import
28from torch.nn import functional as F # pylint: disable=forbidden-backend-import
29
30from hyper_parallel.core.dtensor.dtensor import DTensor
31from hyper_parallel.distributed._builder.forward_rewriter import _ForwardRewriteRequest
32from hyper_parallel.distributed.context_parallel.mla_backward_overlap import (
33 MLAExpandedInputA2AState, MLAInputA2AState,
34 _InputA2ALaunch, _LatentInputA2AWait, _LatentKVLinear,
35 finish_expanded_inputs, launch_input_a2a,
36)
37from hyper_parallel.distributed.context_parallel.gated_mla_backward_overlap import (
38 GatedMLAInputState, finish_gated_expanded_inputs,
39)
40from hyper_parallel.components import functional
41from hyper_parallel.components.modules.mla_attention import prepare_mla_attention_mask
42from hyper_parallel.distributed.context_parallel.collectives import (
43 _build_hybrid_cp_submeshes, async_cp_allgather_launch, async_ulysses_seq_to_head_launch,
44 ulysses_head_to_seq, ulysses_seq_to_head,
45)
46from hyper_parallel.components.functional.npu_fusion_attention import (
47 PACKED_SEQUENCE_ARGUMENTS, npu_fusion_attention_forward, resolve_packed_sequence_lengths,
48)
49from hyper_parallel.core.utils.communication import (
50 differentiable_all_to_all_single,
51 differentiable_all_to_all_single_async, wait_async_tensor,
52)
53
54
55MLACPStrategy = Literal["expanded_ulysses", "latent_kv_head"]
56MLA_CP_STRATEGIES = ("expanded_ulysses", "latent_kv_head")
57
58
59@dataclass(frozen=True)
60class MLADimensions:
61 """Global MLA dimensions; the latent ranks are not partitioned across heads."""
62
63 heads: int
64 q_rank: int
65 kv_rank: int
66 nope_dim: int
67 rope_dim: int
68 value_dim: int
69
70 def __post_init__(self) -> None:
71 for name, value in vars(self).items():
72 if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
73 raise ValueError(f"{name} must be a positive integer, got {value}")
74 if self.rope_dim % 2:
75 raise ValueError("rope_dim must be even")
76
77 @property
78 def qk_dim(self) -> int:
79 """Return the score dimension, including the decoupled RoPE band."""
80 return self.nope_dim + self.rope_dim
81
82
83@dataclass(frozen=True)
84class MLAState:
85 """Normalized local-token latent tensors in BSR layout, before up projection."""
86
87 query_latent: torch.Tensor
88 kv_latent: torch.Tensor
89 key_rope: torch.Tensor
90
91
92@dataclass(frozen=True)
93class MLACPPlan:
94 """Validated explicit execution strategy; no uncalibrated automatic selection."""
95
96 dimensions: MLADimensions
97 cp_degree: int
98 strategy: MLACPStrategy = "expanded_ulysses"
99 tp_degree: int = 1
100 overlap: bool = False
101 ulysses_degree: int | None = None
102 kv_exchange: Literal["all_gather", "all_to_all_v"] = "all_gather"
103
104 def __post_init__(self) -> None:
105 if not isinstance(self.overlap, bool):
106 raise ValueError("overlap must be bool")
107 if self.ulysses_degree is None:
108 object.__setattr__(self, "ulysses_degree", self.cp_degree)
109 for name in ("cp_degree", "tp_degree", "ulysses_degree"):
110 value = getattr(self, name)
111 if not isinstance(value, int) or isinstance(value, bool) or value < 1:
112 raise ValueError(f"{name} must be a positive integer")
113 if self.strategy not in MLA_CP_STRATEGIES:
114 raise ValueError(f"Unknown MLA CP strategy: {self.strategy}")
115 if self.cp_degree % self.ulysses_degree:
116 raise ValueError("ulysses_degree must divide cp_degree")
117 if self.dimensions.heads % (self.tp_degree * self.ulysses_degree):
118 raise ValueError("MLA heads must be divisible by TP degree times Ulysses degree")
119 if self.kv_exchange not in ("all_gather", "all_to_all_v"):
120 raise ValueError("kv_exchange must be all_gather or all_to_all_v")
121 if self.kv_exchange == "all_to_all_v" and self.all_gather_degree == 1:
122 raise ValueError("selective KV exchange requires query-partitioned MLA CP")
123 if self.tp_degree > 1 and self.all_gather_degree > 1:
124 raise ValueError("MLA KV AllGather and hybrid CP currently require TP1")
125
126 @property
127 def local_heads(self) -> int:
128 """Heads in the persistent TP parameter shard, before CP redistribution."""
129 return self.dimensions.heads // self.tp_degree
130
131 @property
132 def compute_heads(self) -> int:
133 """Heads used by this rank's query-partition attention kernel."""
134 return self.local_heads // self.ulysses_degree
135
136 @property
137 def all_gather_degree(self) -> int:
138 """Number of query owners for each Ulysses head partition."""
139 return self.cp_degree // self.ulysses_degree
140
141 def validate_scale(self, scale: float) -> None:
142 """Reject invalid scales without replacing an explicit YaRN scale.
143
144 Args:
145 scale: The model's attention score scale, including any YaRN factor.
146
147 Raises:
148 ValueError: If the scale is nonfinite or nonpositive.
149 """
150 if not math.isfinite(scale) or scale <= 0:
151 raise ValueError("MLA attention scaling must be finite and positive")
152
153
154def mla_all_gather(tensor: torch.Tensor, sequence_dim: int, cp_mesh: Any) -> torch.Tensor:
155 """Gather source tokens and sum all consumer gradients back to their owner.
156
157 Args:
158 tensor: Equal-sized local sequence shard.
159 sequence_dim: Dimension along which source shards are concatenated.
160 cp_mesh: CP mesh, or None for communication-free execution.
161
162 Returns:
163 Full-sequence tensor in CP group-rank order.
164 """
165 if cp_mesh is None or cp_mesh.size() == 1:
166 return tensor
167 # Share the overlap path's SUM ReduceScatter adjoint, but wait immediately.
168 # PyTorch's generic autograd AllGather falls back to AllToAll + local sum on
169 # HCCL, changing BF16 reduction order when only the overlap flag changes.
170 return async_cp_allgather_launch(tensor, sequence_dim, cp_mesh).wait()
171
172
173def apply_mla_rope(
174 tensor: torch.Tensor,
175 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None,
176 *,
177 interleaved: bool = False,
178) -> torch.Tensor:
179 """Rotate one local BSND tensor so Q and shared K can be scheduled independently."""
180 if position_embeddings is None:
181 return tensor
182 cos, sin = (part.to(tensor.dtype) for part in position_embeddings)
183 if cos.ndim == 2:
184 cos, sin = cos.unsqueeze(0), sin.unsqueeze(0)
185 if interleaved:
186 cos, sin = (part[..., :tensor.shape[-1] // 2].repeat_interleave(2, dim=-1)
187 for part in (cos, sin))
188 cos, sin = (part.expand(tensor.shape[0], -1, -1).transpose(0, 1) for part in (cos, sin))
189 rotated = functional.apply_rotary_pos_emb_single(
190 tensor.transpose(0, 1), cos, sin,
191 rotary_interleaved=interleaved, use_fused_rotary_pos_emb=True,
192 )
193 if interleaved:
194 # V3 consumes real/imaginary half-bands; the kernel returns adjacent pairs.
195 rotated = rotated.permute(1, 2, 0, 3)
196 return rotated.unflatten(-1, (-1, 2)).transpose(-1, -2).flatten(-2).transpose(1, 2)
197 return rotated.transpose(0, 1)
198
199
200def _validate_query_partition(causal, attention_mask):
201 """Reject query-partition modes whose FA metadata contract is not implemented."""
202 if not causal or attention_mask is not None:
203 raise ValueError("MLA KV AllGather and hybrid CP require causal attention without an explicit mask")
204
205
206@dataclass(frozen=True)
207class _CausalPartition:
208 """One continuous query partition and its document-local causal KV prefixes."""
209
210 key_begin: int
211 key_end: int
212 query_lengths: tuple[int, ...]
213 key_lengths: tuple[int, ...]
214 documents: tuple[int, ...]
215
216 def select(self, tensor: torch.Tensor, sequence_dim: int) -> torch.Tensor:
217 """Keep a view of the needed prefix before either projection or attention.
218
219 Args:
220 tensor: Gathered KV features in global sequence order.
221 sequence_dim: Axis containing the gathered tokens.
222
223 Returns:
224 A view of the KV interval needed by this query partition.
225 """
226 return tensor.narrow(sequence_dim, self.key_begin, self.key_end - self.key_begin)
227
228
229def _causal_partition(lengths, query_length, query_rank, batch) -> _CausalPartition:
230 """Coalesce adjacent document prefixes without copying their KV tensors."""
231 query_begin = query_rank * query_length
232 query_end = query_begin + query_length
233 query_ends, key_ends = [], []
234 query_count = key_count = document_begin = 0
235 key_begin = None
236 for document_end in lengths:
237 begin, end = max(document_begin, query_begin), min(document_end, query_end)
238 if begin < end:
239 if key_begin is None:
240 key_begin = document_begin
241 query_count += end - begin
242 key_count += end - document_begin
243 query_ends.append(query_count)
244 key_ends.append(key_count)
245 document_begin = document_end
246 if query_count != query_length:
247 raise ValueError("MLA document boundaries do not cover the query partition")
248 # Continuous Q ownership makes all selected document prefixes adjacent.
249 return _CausalPartition(
250 key_begin, query_end,
251 tuple(end + index * query_count for index in range(batch) for end in query_ends),
252 tuple(end + index * key_count for index in range(batch) for end in key_ends),
253 tuple(lengths),
254 )
255
256
257@lru_cache(maxsize=128)
258def _kv_exchange_ranges(documents, source_length, query_length, head_copies, degree, rank):
259 """Cache only host intervals shared by layers; never retain activation tensors."""
260 destinations = tuple(
261 _causal_partition(documents, query_length, peer // head_copies, 1)
262 for peer in range(degree)
263 )
264 start = rank * source_length
265 sends, receives = [], []
266 local = destinations[rank]
267 for peer, destination in enumerate(destinations):
268 begin, end = max(start, destination.key_begin), min(start + source_length, destination.key_end)
269 sends.append((begin - start, end - begin) if begin < end else (0, 0))
270 begin, end = max(peer * source_length, local.key_begin), min((peer + 1) * source_length, local.key_end)
271 receives.append(max(0, end - begin))
272 local_only = all(peer * source_length <= item.key_begin and item.key_end <= (peer + 1) * source_length
273 for peer, item in enumerate(destinations))
274 return tuple(sends), tuple(receives), local_only
275
276
277@dataclass
278class _SelectiveKVExchange:
279 """A shared differentiable A2Av and its batch-major receive layout."""
280
281 output: torch.Tensor
282 receive_tokens: tuple[int, ...]
283 batch: int
284
285 def wait(self) -> torch.Tensor:
286 """Finish communication before rebuilding batch-major token order."""
287 if not self.receive_tokens:
288 return self.output
289 output = wait_async_tensor(self.output)
290 parts, start = [], 0
291 for count in self.receive_tokens:
292 rows = self.batch * count
293 if count:
294 parts.append(output.narrow(0, start, rows).reshape(self.batch, count, *output.shape[1:]))
295 start += rows
296 return parts[0] if len(parts) == 1 else torch.cat(parts, dim=1)
297
298
299def _selective_kv_launch(payload, partition, mesh, head_copies, overlap):
300 """Send only each query owner's causal document interval, with SUM gradients through repeated slices."""
301 batch, source_length = payload.shape[:2]
302 rank, degree = mesh.get_local_rank(), mesh.size()
303 sends, receives, local_only = _kv_exchange_ranges(
304 partition.documents, source_length, partition.query_lengths[-1] // batch, head_copies, degree, rank,
305 )
306 if local_only:
307 begin, count = sends[rank]
308 return _SelectiveKVExchange(payload.narrow(1, begin, count), (), batch)
309 send = torch.cat([payload.narrow(1, begin, count).flatten(0, 1) for begin, count in sends], dim=0)
310 input_splits = [batch * count for _, count in sends]
311 output_splits = [batch * count for count in receives]
312 exchange = differentiable_all_to_all_single_async if overlap else differentiable_all_to_all_single
313 output = exchange(send, input_splits, output_splits, mesh.get_group())
314 return _SelectiveKVExchange(output, receives, batch)
315
316
317def _positions(
318 embeddings: tuple[torch.Tensor, torch.Tensor] | None,
319 state: MLAState,
320 rope_dim: int,
321) -> tuple[torch.Tensor, torch.Tensor] | None:
322 """Validate local-token RoPE frequencies and broadcast only the batch axis."""
323 if embeddings is None:
324 return None
325 if not isinstance(embeddings, (tuple, list)) or len(embeddings) != 2:
326 raise ValueError("position_embeddings must be a local (cos, sin) pair")
327 batch, sequence = state.query_latent.shape[:2]
328 parts = []
329 for tensor in embeddings:
330 if tensor.ndim == 2:
331 tensor = tensor.unsqueeze(0)
332 if tensor.shape not in ((1, sequence, rope_dim), (batch, sequence, rope_dim)):
333 raise ValueError("RoPE cos/sin must describe local tokens with shape [B, S_local, rope_dim]")
334 if tensor.requires_grad:
335 raise ValueError("MLA CP currently requires non-trainable RoPE frequencies")
336 if tensor.device != state.query_latent.device:
337 raise ValueError("RoPE and MLA activations must be on the same device")
338 parts.append(tensor.expand(batch, -1, -1))
339 return tuple(parts)
340
341
342def _validate_mask(
343 mask: torch.Tensor | None,
344 lengths: Sequence[int] | None,
345 batch: int,
346 sequence: int,
347 device: torch.device,
348) -> None:
349 """Require global boolean masks and reject unsupported packed-mask combinations."""
350 if mask is None:
351 return
352 if lengths is not None:
353 raise ValueError("packed MLA CP currently accepts document causal/noncausal masks only")
354 if mask.dtype != torch.bool:
355 raise ValueError("MLA CP masks must be boolean (True=allowed); additive bias is not supported")
356 valid = (
357 mask.ndim == 2 and mask.shape == (sequence, sequence)
358 or mask.ndim == 4 and mask.shape[0] in (1, batch)
359 and mask.shape[1] == 1 and mask.shape[-2:] == (sequence, sequence)
360 )
361 if not valid or mask.device != device:
362 raise ValueError("attention_mask must cover global Q/K: [S, S] or [B|1, 1, S, S], on the input device")
363
364
365def _query(
366 module: nn.Module,
367 latent: torch.Tensor,
368 positions: tuple[torch.Tensor, torch.Tensor] | None,
369 heads: int,
370 rotary_fn: Callable[..., torch.Tensor],
371) -> torch.Tensor:
372 """Expand queries through the model's rotary callback and managed projection."""
373 projected = module.q_b_proj(latent)
374 projected = projected.view(*latent.shape[:2], heads, module.qk_head_dim)
375 content, rotary = projected.split((module.qk_nope_head_dim, module.qk_rope_head_dim), dim=-1)
376 rotated = rotary_fn(rotary, positions, interleaved=module.rotary_interleaved)
377 # An identity callback needs no split/concatenate copy of the complete Q.
378 if rotated is rotary:
379 return projected.transpose(1, 2)
380 return torch.cat((content, rotated), dim=-1).transpose(1, 2)
381
382
383def _key_value(
384 module: nn.Module,
385 latent: torch.Tensor,
386 key_rope: torch.Tensor,
387 heads: int,
388 head_range: tuple[int, int] | None,
389) -> tuple[torch.Tensor, torch.Tensor]:
390 """Expand the requested KV heads through the managed projection module."""
391 projected = (module.kv_b_proj(latent) if head_range is None
392 else module.kv_b_proj(latent, mla_head_range=head_range))
393 return _assemble_key_value(module, projected, key_rope, heads)
394
395
396def _assemble_key_value(module, projected, key_rope, heads):
397 """Build per-head keys and values from shared projection results."""
398 projected = projected.view(*projected.shape[:2], heads, module.qk_nope_head_dim + module.v_head_dim)
399 content, value = projected.split((module.qk_nope_head_dim, module.v_head_dim), dim=-1)
400 key = torch.cat((content, key_rope.unsqueeze(2).expand(-1, -1, heads, -1)), dim=-1)
401 return key.transpose(1, 2), value.transpose(1, 2)
402
403
404def _validate_forward_kwargs(kwargs, position_embeddings):
405 """Validate forwarded model options before routing any latent."""
406 position_ids = kwargs.pop("position_ids", None)
407 if position_ids is not None and position_embeddings is None:
408 raise ValueError("position_ids require precomputed local position_embeddings in MLA CP")
409 kwargs.pop("cache_position", None)
410 if kwargs.pop("use_cache", False) or kwargs.pop("output_attentions", False):
411 raise ValueError("MLA CP does not return KV caches or attention weights")
412 if kwargs:
413 raise ValueError(f"Unsupported MLA CP forward arguments: {sorted(kwargs)}")
414
415
416def _validate_state(module: nn.Module, plan: MLACPPlan, state: MLAState) -> None:
417 """Check latent shape and NPU kernel requirements before collectives."""
418 dimensions = plan.dimensions
419 query_latent, kv_latent, key_rope = state.query_latent, state.kv_latent, state.key_rope
420 if query_latent.ndim != 3:
421 raise ValueError("MLA CP latents must have BSR layout")
422 batch, sequence = query_latent.shape[:2]
423 if batch < 1 or sequence < 1:
424 raise ValueError("MLA CP requires nonempty batches and sequence shards")
425 for tensor, width in ((query_latent, dimensions.q_rank), (kv_latent, dimensions.kv_rank),
426 (key_rope, dimensions.rope_dim)):
427 if (tensor.shape != (batch, sequence, width) or tensor.device != query_latent.device
428 or tensor.dtype != query_latent.dtype):
429 raise ValueError("MLA CP latent shapes/devices/dtypes do not match the plan")
430 if module.training and module.attention_dropout != 0:
431 raise ValueError("MLA CP requires attention dropout=0")
432 plan.validate_scale(module.scaling)
433 if query_latent.device.type != "npu" or query_latent.dtype not in (torch.float16, torch.bfloat16):
434 raise ValueError("NPU MLA CP requires NPU BF16/FP16 activations")
435 if dimensions.value_dim > dimensions.qk_dim:
436 raise ValueError("NPU MLA CP requires value_dim <= qk_dim")
437
438
439class MLAContextParallel:
440 """Run CP over normalized latents; parameter-gradient reduction belongs to the trainer."""
441
442 def __init__(
443 self,
444 plan: MLACPPlan,
445 cp_mesh: Any,
446 *,
447 rotary_fn: Callable[..., torch.Tensor] | None = None,
448 attention_interface: Callable[..., tuple[torch.Tensor, Any]] | None = None,
449 ) -> None:
450 """Bind communication to optional local rotary and attention implementations.
451
452 Args:
453 plan: Validated MLA communication and head plan.
454 cp_mesh: Framework-owned CP mesh, or None for degree one.
455 rotary_fn: Local RoPE callback receiving tensor, positions and interleaved.
456 It preserves the tensor's shape, dtype and device. None uses generic RoPE.
457 attention_interface: Local attention callback with the generic attention contract.
458 It returns BSND output and optional weights. Extra keywords mla_head_offset
459 and mla_local_heads describe its head slice within the TP-local heads.
460 None uses the existing NPU attention implementation.
461
462 Note:
463 Callbacks can run again during recomputation; their side effects belong to the caller.
464 Communication, projections and output restoration remain owned by this runtime.
465 """
466 if (1 if cp_mesh is None else cp_mesh.size()) != plan.cp_degree:
467 raise ValueError("MLACPPlan degree must match the CP mesh")
468 if rotary_fn is not None and not callable(rotary_fn):
469 raise TypeError("rotary_fn must be callable")
470 if attention_interface is not None and not callable(attention_interface):
471 raise TypeError("attention_interface must be callable")
472 self.plan = plan
473 self.rotary_fn = rotary_fn
474 self.attention_interface = attention_interface
475 self.cp_mesh = cp_mesh
476 rank = 0 if cp_mesh is None else cp_mesh.get_local_rank()
477 self.query_rank, self.head_rank = divmod(rank, plan.ulysses_degree)
478 self.ulysses_mesh = self.all_gather_mesh = cp_mesh
479 if 1 < plan.ulysses_degree < plan.cp_degree:
480 self.ulysses_mesh, self.all_gather_mesh = _build_hybrid_cp_submeshes(cp_mesh, plan.ulysses_degree)
481
482 def __call__(
483 self,
484 module: nn.Module,
485 query_latent: torch.Tensor,
486 kv_latent: torch.Tensor,
487 key_rope: torch.Tensor,
488 *,
489 position_embeddings: tuple[torch.Tensor, torch.Tensor] | None = None,
490 attention_mask: torch.Tensor | None = None,
491 actual_seq_len: torch.Tensor | Sequence[int] | None = None,
492 is_causal: bool | None = None,
493 restore_output: bool = True,
494 overlap_input_backward: bool = False,
495 input_gate: GatedMLAInputState | None = None,
496 **kwargs: Any,
497 ) -> tuple[torch.Tensor, None]:
498 """Run attention and optionally restore its output to token-local BSND.
499
500 Args:
501 overlap_input_backward: Pair input autograd nodes only for pure Ulysses.
502 AG/hybrid input gradients remain native; output backward is selected
503 independently by the model adapter.
504 input_gate: Model-owned gate to cover expanded pure-Ulysses input exchanges.
505 restore_output: False exposes the head-sharded output so model-owned
506 independent work can run before its return exchange completes.
507 """
508 packed_kwargs = {name: kwargs.pop(name) for name in PACKED_SEQUENCE_ARGUMENTS if name in kwargs}
509 packed_kwargs["actual_seq_len"] = actual_seq_len
510 _validate_forward_kwargs(kwargs, position_embeddings)
511 state = MLAState(query_latent, kv_latent, key_rope)
512 dimensions, degree = self.plan.dimensions, self.plan.cp_degree
513 batch, sequence = query_latent.shape[:2]
514 lengths, key_lengths = resolve_packed_sequence_lengths(
515 packed_kwargs, batch * sequence * degree, batch * sequence * degree,
516 )
517 if lengths is not None and batch != 1:
518 raise ValueError("packed MLA CP currently requires batch_size=1")
519 if lengths != key_lengths:
520 raise ValueError("MLA CP requires matching global query and key document boundaries")
521 causal = getattr(module, "is_causal", True) if is_causal is None else is_causal
522 if not isinstance(causal, bool):
523 raise ValueError("is_causal must be bool")
524 _validate_mask(attention_mask, lengths, batch, sequence * degree, query_latent.device)
525 attention_mask = prepare_mla_attention_mask(attention_mask, sequence * degree, sequence * degree)
526 partition = None
527 if self.plan.all_gather_degree > 1:
528 _validate_query_partition(causal, attention_mask)
529 partition = _causal_partition(
530 lengths or (sequence * degree,), sequence * self.plan.ulysses_degree, self.query_rank, batch,
531 )
532 lengths, key_lengths = partition.query_lengths, partition.key_lengths
533 positions = _positions(position_embeddings, state, dimensions.rope_dim)
534 _validate_state(module, self.plan, state)
535 rotary_fn = apply_mla_rope if self.rotary_fn is None else self.rotary_fn
536 rotated_key = rotary_fn(
537 state.key_rope.unsqueeze(2), positions, interleaved=module.rotary_interleaved,
538 ).squeeze(2)
539 query, key, value = self._prepare_attention_inputs(
540 module, state, positions, rotated_key, partition, overlap_input_backward, input_gate, rotary_fn,
541 )
542 attention_interface = (
543 npu_fusion_attention_forward if self.attention_interface is None else self.attention_interface
544 )
545 output = attention_interface(
546 module, query, key, value, attention_mask, scaling=module.scaling,
547 is_causal=causal, actual_seq_qlen=lengths, actual_seq_kvlen=key_lengths,
548 pre_tokens=2147483647, next_tokens=0 if causal else 2147483647,
549 mla_head_offset=self.head_rank * self.plan.compute_heads,
550 mla_local_heads=self.plan.local_heads,
551 )[0]
552 if restore_output:
553 output = self.restore_output(output)
554 return output, None
555
556 def _prepare_attention_inputs(self, module, state, positions, key_rope, partition,
557 overlap_input_backward, input_gate, rotary_fn):
558 """Select input preparation; paired input backward requires pure Ulysses.
559
560 Forward overlap also supports KV AllGather and hybrid CP. Those paths
561 retain native input collective adjoints; their output dO overlap is
562 selected independently by the model adapter.
563 """
564 use_forward_overlap = self.plan.overlap and self.plan.cp_degree > 1
565 pure_ulysses = self.plan.all_gather_degree == 1
566 expanded = self.plan.strategy == "expanded_ulysses"
567 supports_input_gate = use_forward_overlap and pure_ulysses and expanded
568 if input_gate is not None and not supports_input_gate:
569 raise ValueError("Input gate overlap requires expanded pure Ulysses with overlap enabled")
570 if overlap_input_backward and not pure_ulysses:
571 raise ValueError("Paired MLA input backward currently requires pure Ulysses")
572 if not use_forward_overlap:
573 query = _query(module, state.query_latent, positions, self.plan.local_heads, rotary_fn)
574 prepare = self._expanded if expanded or self.plan.cp_degree == 1 else self._latent
575 return prepare(module, state, query, key_rope, partition)
576 # Pure Ulysses has no outer KV AllGather. Expanded Q/K/V use the same
577 # V/K/Q forward order with or without a gate; overlap_input_backward
578 # selects paired input gradients versus the native collective adjoints.
579 if pure_ulysses and expanded:
580 return self._prepare_expanded_ulysses_inputs(
581 module, state, positions, key_rope, input_gate, overlap_input_backward, rotary_fn,
582 )
583 if overlap_input_backward:
584 # Expanded has returned above and AG/hybrid were rejected earlier:
585 # this is latent pure Ulysses, pairing Q A2A with KV projection/RS.
586 return self._prepare_latent_ulysses_inputs(module, state, positions, key_rope, rotary_fn)
587 # AG/hybrid inputs and forward-only latent Ulysses use native adjoints.
588 prepare = self._expanded_overlap if expanded else self._latent_overlap
589 return prepare(module, state, positions, key_rope, partition, rotary_fn=rotary_fn)
590
591 def restore_output(self, output: torch.Tensor) -> torch.Tensor:
592 """Restore token ownership after attention, before the model output projection.
593
594 Args:
595 output: Attention result in BSND head-sharded layout.
596
597 Returns:
598 Original local-token, all-local-head BSND tensor.
599 """
600 batch, sequence = output.shape[:2]
601 sequence //= self.plan.ulysses_degree
602 if self.plan.ulysses_degree > 1:
603 output = ulysses_head_to_seq(output, 1, 2, self.ulysses_mesh)
604 expected = (batch, sequence, self.plan.local_heads, self.plan.dimensions.value_dim)
605 if tuple(output.shape) != expected:
606 raise RuntimeError(f"MLA CP output has shape {tuple(output.shape)}, expected {expected}")
607 return output
608
609
610 def _expanded(self, module, state, query, key_rope, partition=None):
611 """Pack unequal-width QKV into one sequence-to-head exchange."""
612 key, value = _key_value(module, state.kv_latent, key_rope, self.plan.local_heads, None)
613 if self.plan.ulysses_degree > 1:
614 widths = (query.shape[-1], key.shape[-1], value.shape[-1])
615 payload = torch.cat(tuple(tensor.transpose(1, 2) for tensor in (query, key, value)), dim=-1)
616 payload = ulysses_seq_to_head(payload, 1, 2, self.ulysses_mesh)
617 query, key, value = (tensor.transpose(1, 2) for tensor in payload.split(widths, dim=-1))
618 if self.plan.all_gather_degree > 1:
619 widths = (key.shape[-1], value.shape[-1])
620 payload = torch.cat((key.transpose(1, 2), value.transpose(1, 2)), dim=-1)
621 if self.plan.kv_exchange == "all_to_all_v":
622 payload = _selective_kv_launch(payload, partition, self.all_gather_mesh, 1, False).wait()
623 else:
624 payload = partition.select(mla_all_gather(payload, 1, self.all_gather_mesh), 1)
625 key, value = (tensor.transpose(1, 2) for tensor in payload.split(widths, dim=-1))
626 return query, key, value
627
628 def _latent(self, module, state, query, key_rope, partition=None):
629 """Exchange Q heads, gather shared KV latents, then expand only owned heads."""
630 if self.plan.ulysses_degree > 1:
631 query = ulysses_seq_to_head(query, 2, 1, self.ulysses_mesh)
632 payload = torch.cat((state.kv_latent, key_rope), dim=-1)
633 if self.plan.kv_exchange == "all_to_all_v":
634 payload = _selective_kv_launch(payload, partition, self.cp_mesh, self.plan.ulysses_degree, False).wait()
635 partition = None
636 else:
637 payload = mla_all_gather(payload, 1, self.cp_mesh)
638 return query, *self._expand_latent(module, payload, partition)
639
640 def _expand_latent(self, module, payload, partition=None):
641 """Keep the same managed head-selective projection for both communication schedules."""
642 if partition is not None:
643 payload = partition.select(payload, 1)
644 dimensions = self.plan.dimensions
645 heads = self.plan.compute_heads
646 start = self.head_rank * heads
647 kv_latent, key_rope = payload.split((dimensions.kv_rank, dimensions.rope_dim), dim=-1)
648 # A contiguous latent preserves the native Linear bias-fusion path after payload splitting.
649 return _key_value(module, kv_latent.contiguous(), key_rope, heads, (start, start + heads))
650
651 def _expanded_overlap(self, module, state, positions, key_rope, partition=None, *, rotary_fn):
652 """Overlap KV transfer with Q preparation, respecting the two-stage hybrid dependency."""
653 key, value = _key_value(module, state.kv_latent, key_rope, self.plan.local_heads, None)
654 payload = torch.cat((key.transpose(1, 2), value.transpose(1, 2)), dim=-1)
655 use_ulysses = self.plan.ulysses_degree > 1
656 if use_ulysses:
657 kv_transfer_handle = async_ulysses_seq_to_head_launch(payload, 1, 2, self.ulysses_mesh)
658 else:
659 kv_transfer_handle = self._expanded_kv_launch(payload, partition)
660 query = _query(module, state.query_latent, positions, self.plan.local_heads, rotary_fn)
661 query_a2a_handle = (async_ulysses_seq_to_head_launch(query.transpose(1, 2), 1, 2, self.ulysses_mesh)
662 if use_ulysses else None)
663 payload = kv_transfer_handle.wait()
664 if use_ulysses and self.plan.all_gather_degree > 1:
665 payload = self._expanded_kv_launch(payload, partition).wait()
666 if query_a2a_handle is not None:
667 query = query_a2a_handle.wait().transpose(1, 2)
668 if partition is not None and self.plan.kv_exchange == "all_gather":
669 payload = partition.select(payload, 1)
670 key, value = payload.split((module.qk_head_dim, module.v_head_dim), dim=-1)
671 return query, key.transpose(1, 2), value.transpose(1, 2)
672
673 def _expanded_kv_launch(self, payload, partition):
674 """Complete the sequence dimension after Ulysses has selected this head shard."""
675 if self.plan.kv_exchange == "all_to_all_v":
676 return _selective_kv_launch(payload, partition, self.all_gather_mesh, 1, True)
677 return async_cp_allgather_launch(payload, 1, self.all_gather_mesh)
678
679 def _latent_overlap(self, module, state, positions, key_rope, partition=None, *, rotary_fn):
680 """Cover latent transfer with Q preparation, then Q A2A with KV expansion."""
681 payload = torch.cat((state.kv_latent, key_rope), dim=-1)
682 if self.plan.kv_exchange == "all_to_all_v":
683 kv_transfer_handle = _selective_kv_launch(payload, partition, self.cp_mesh, self.plan.ulysses_degree, True)
684 partition = None
685 else:
686 kv_transfer_handle = async_cp_allgather_launch(payload, 1, self.cp_mesh)
687 query = _query(module, state.query_latent, positions, self.plan.local_heads, rotary_fn)
688 payload = kv_transfer_handle.wait()
689 query_a2a_handle = (async_ulysses_seq_to_head_launch(query.transpose(1, 2), 1, 2, self.ulysses_mesh)
690 if self.plan.ulysses_degree > 1 else None)
691 key, value = self._expand_latent(module, payload, partition)
692 if query_a2a_handle is not None:
693 query = query_a2a_handle.wait().transpose(1, 2)
694 return query, key, value
695
696 def _prepare_expanded_ulysses_inputs(self, module, state, positions, key_rope, input_gate,
697 overlap_input_backward, rotary_fn):
698 """Prepare expanded Q/K/V in V/K/Q order and attach the selected adjoints."""
699 launch_input = _InputA2ALaunch.apply if overlap_input_backward else launch_input_a2a
700 input_a2a = MLAExpandedInputA2AState(
701 query=MLAInputA2AState(self.ulysses_mesh),
702 key=MLAInputA2AState(self.ulysses_mesh),
703 value=MLAInputA2AState(self.ulysses_mesh),
704 )
705 # V needs no positional transform: it is ready as soon as KV up-projection
706 # is split. Shared-K positional math was already applied by the caller.
707 projected = module.kv_b_proj(state.kv_latent)
708 projected = projected.view(*state.kv_latent.shape[:2], self.plan.local_heads,
709 module.qk_nope_head_dim + module.v_head_dim)
710 key_nope, value = projected.split((module.qk_nope_head_dim, module.v_head_dim), dim=-1)
711 # Keep K assembly inside V's communication window.
712 key_nope = launch_input(value, key_nope, input_a2a.value)
713 shared_key = key_rope.unsqueeze(2).expand(-1, -1, self.plan.local_heads, -1)
714 key = torch.cat((key_nope, shared_key), dim=-1)
715 # K is complete; Q projection and its model-defined rotation can cover it.
716 query_latent = launch_input(key, state.query_latent, input_a2a.key)
717 query = _query(module, query_latent, positions, self.plan.local_heads, rotary_fn).transpose(1, 2)
718 # Only gated MLA has independent compute after Q. Both consumers share
719 # the Q/K/V transfer state, but return distinct Q or gate gradient edges.
720 if input_gate is None:
721 query, key, value = finish_expanded_inputs(query, input_a2a, overlap_input_backward)
722 else:
723 query, key, value = finish_gated_expanded_inputs(query, input_a2a, input_gate)
724 return query.transpose(1, 2), key.transpose(1, 2), value.transpose(1, 2)
725
726 def _prepare_latent_ulysses_inputs(self, module, state, positions, key_rope, rotary_fn):
727 """Prepare latent Ulysses inputs and pair their Q/KV backward dependencies."""
728 payload = torch.cat((state.kv_latent, key_rope), dim=-1)
729 kv_all_gather_handle = async_cp_allgather_launch(payload, 1, self.cp_mesh)
730 query = _query(module, state.query_latent, positions, self.plan.local_heads, rotary_fn).transpose(1, 2)
731 # wait() normally adds the default AllGather autograd node. Instead,
732 # _LatentKVLinear launches SUM RS before KV wgrad and returns dPayload
733 # via kv_input. Keep this numeric buffer off the default gradient path.
734 with torch.no_grad():
735 gathered = kv_all_gather_handle.wait()
736 query_a2a = MLAInputA2AState(self.ulysses_mesh)
737 # Same payload value, with Q's inverse A2A bracketing its KV backward.
738 kv_input = _InputA2ALaunch.apply(query, payload, query_a2a)
739 heads = self.plan.compute_heads
740 start = self.head_rank * heads
741 projected, shared_key = module.kv_b_proj(
742 kv_input, mla_head_range=(start, start + heads),
743 mla_gathered=gathered, mla_cp_mesh=self.cp_mesh,
744 )
745 key, value = _assemble_key_value(module, projected, shared_key, heads)
746 query, key, value = _LatentInputA2AWait.apply(key, value, query_a2a)
747 return query.transpose(1, 2), key, value
748
749
750def _head_projection_request(projection: nn.Linear, head_dim: int) -> _ForwardRewriteRequest:
751 """Select head rows inside the original Linear call to preserve parameter hooks."""
752 original = projection.forward
753
754 # Keep nn.Linear.forward(input=...) compatible with keyword callers.
755 @wraps(original)
756 def head_projection( # pylint: disable=redefined-builtin
757 input: torch.Tensor, *, mla_head_range: tuple[int, int] | None = None,
758 mla_gathered: torch.Tensor | None = None, mla_cp_mesh: Any = None,
759 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
760 """Project selected contiguous heads inside the original module call.
761
762 Args:
763 input: Latent activations, or the local packed payload when mla_gathered is provided.
764 mla_head_range: Optional half-open range of TP-local heads.
765 mla_gathered: Completed latent payload for scheduled gather backward.
766 mla_cp_mesh: CP mesh owning the gathered payload.
767
768 Returns:
769 Projected activations, or their pair with the shared key for scheduled gather backward.
770 """
771 if mla_head_range is None:
772 return original(input)
773 begin, end = mla_head_range
774 if not 0 <= begin < end <= projection.weight.shape[0] // head_dim:
775 raise ValueError("MLA projection head range is outside the original parameter")
776 rows = slice(begin * head_dim, end * head_dim)
777 bias = None if projection.bias is None else projection.bias[rows]
778 # Read parameters inside the managed module's forward, after pre-hooks.
779 # Slicing preserves the Parameter identity and scatters gradients back.
780 if mla_gathered is not None:
781 if mla_cp_mesh is None:
782 raise ValueError("Scheduled MLA latent projection requires its CP mesh")
783 return _LatentKVLinear.apply(input, projection.weight[rows], bias, mla_gathered, mla_cp_mesh)
784 return F.linear(input, projection.weight[rows], bias) # pylint: disable=not-callable
785
786 return _ForwardRewriteRequest(projection, head_projection)
787
788
789def _validate_up_projections(target_module: nn.Module, plan: MLACPPlan) -> None:
790 """Check TP-local Linear weights without replacing managed parameters."""
791 dimensions = plan.dimensions
792 projections = (
793 (target_module.q_b_proj, dimensions.q_rank, dimensions.qk_dim),
794 (target_module.kv_b_proj, dimensions.kv_rank, dimensions.nope_dim + dimensions.value_dim),
795 )
796 for projection, in_features, head_dim in projections:
797 # Quantized/LoRA subclasses do not obey contiguous Linear head slicing.
798 if type(projection) is not nn.Linear: # pylint: disable=unidiomatic-typecheck
799 raise ValueError("MLA CP requires plain nn.Linear up projections")
800 weight = projection.weight.to_local() if isinstance(projection.weight, DTensor) else projection.weight
801 if weight.shape != (plan.local_heads * head_dim, in_features):
802 raise ValueError("MLA up-projection shape must match TP-local heads; apply TP parameter sharding first")
803 if getattr(projection.forward, "__func__", None) is not nn.Linear.forward:
804 raise ValueError("MLA CP must be installed before other up-projection forward adapters")
805
806
807def prepare_mla_cp(
808 target_module: nn.Module,
809 cp_mesh: Any,
810 tp_mesh: Any,
811 *,
812 strategy: MLACPStrategy,
813 overlap: bool,
814 ulysses_degree: int | None,
815 kv_exchange: str,
816 rotary_fn: Callable[..., torch.Tensor] | None = None,
817 attention_interface: Callable[..., tuple[torch.Tensor, Any]] | None = None,
818) -> tuple[MLAContextParallel, list[_ForwardRewriteRequest]]:
819 """Prepare a shared MLA runtime and managed head-projection rewrites.
820
821 Args:
822 target_module: Module exposing normalized MLA latents and plain projections.
823 cp_mesh: Context-parallel mesh.
824 tp_mesh: Optional tensor-parallel mesh.
825 strategy: Expanded or latent KV representation.
826 overlap: Whether to overlap input communication with projections.
827 ulysses_degree: Head-parallel degree within CP.
828 kv_exchange: Full gather or selective KV exchange.
829 rotary_fn: Model-owned rotary or identity callback.
830 attention_interface: Optional model-local attention implementation.
831
832 Returns:
833 Runtime and child rewrites; the model adapter owns its forward contract.
834 """
835 for name, axis in (("TP", tp_mesh), ("CP", cp_mesh)):
836 if axis is not None and (axis.size() < 1 or getattr(axis, "ndim", 1) != 1):
837 raise ValueError(f"mla_cp_wrapper requires a one-dimensional {name} mesh")
838 cp_degree = 1 if cp_mesh is None else cp_mesh.size()
839 tp_degree = 1 if tp_mesh is None else tp_mesh.size()
840 if getattr(target_module, "mla_cp_runtime", None) is not None:
841 raise ValueError("MLA CP is already configured on this module")
842 if target_module.attention_dropout != 0 or target_module.sliding_window is not None:
843 raise ValueError("MLA CP requires dropout=0 and no implicit sliding_window; use a supported boolean mask")
844 dimensions = MLADimensions(
845 heads=target_module.config.num_attention_heads, q_rank=target_module.q_lora_rank,
846 kv_rank=target_module.kv_lora_rank, nope_dim=target_module.qk_nope_head_dim,
847 rope_dim=target_module.qk_rope_head_dim, value_dim=target_module.v_head_dim,
848 )
849 plan = MLACPPlan(
850 dimensions=dimensions, cp_degree=cp_degree, strategy=strategy,
851 tp_degree=tp_degree, overlap=overlap, ulysses_degree=ulysses_degree, kv_exchange=kv_exchange,
852 )
853 plan.validate_scale(target_module.scaling)
854 _validate_up_projections(target_module, plan)
855 runtime = MLAContextParallel(
856 plan, cp_mesh, rotary_fn=rotary_fn, attention_interface=attention_interface,
857 )
858
859 requests = []
860 if strategy == "latent_kv_head":
861 requests.append(_head_projection_request(target_module.kv_b_proj, dimensions.nope_dim + dimensions.value_dim))
862 return runtime, requests