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"""MLA backward scheduling with paired input A2A nodes and one output projection node.
16
17Paired launch/wait nodes require a complete first-order training backward.
18Partial autograd.grad calls can prune the consumer and are not supported.
19"""
20
21from __future__ import annotations
22
23from dataclasses import dataclass
24from functools import wraps
25from typing import Any
26
27# This CP scheduler uses the native Torch autograd and communication APIs.
28import torch # pylint: disable=forbidden-backend-import
29import torch.distributed as dist # pylint: disable=forbidden-backend-import
30from torch import nn # pylint: disable=forbidden-backend-import
31from torch.autograd.function import once_differentiable # pylint: disable=forbidden-backend-import
32from torch.nn import functional as F # pylint: disable=forbidden-backend-import
33
34from hyper_parallel.distributed._builder.forward_rewriter import _ForwardRewriteRequest
35from hyper_parallel.distributed.context_parallel.collectives import (
36 AsyncCPCollective, async_ulysses_seq_to_head_launch,
37)
38
39
40@dataclass
41class MLAOutputProjectionState:
42 """Output layout for ordinary MLA; its projection node owns O A2A."""
43
44 mesh: Any
45 head_dim: int
46
47
48class _OutputProjection(torch.autograd.Function):
49 """Own the ordinary output Linear derivative and its dO A2A."""
50
51 @staticmethod
52 def forward(ctx: Any, attention_output: torch.Tensor, weight: torch.Tensor,
53 bias: torch.Tensor | None, state: MLAOutputProjectionState) -> torch.Tensor:
54 """Restore the FA output to local tokens, then apply Wo.
55
56 Args:
57 ctx: Autograd context for operands, layout and autocast settings.
58 attention_output: FA output in [batch, sequence, local_heads, value_dim].
59 weight: Managed output-projection weight.
60 bias: Optional output-projection bias.
61 state: Ulysses output layout owned by this projection node.
62
63 Returns:
64 Projected local hidden states.
65 """
66 output_a2a_handle = async_ulysses_seq_to_head_launch(attention_output, 2, 1, state.mesh)
67 # Function.forward owns both the numeric A2A and its backward. The
68 # collective's internal autograd wrapper is not recorded in this scope.
69 local_output = output_a2a_handle.wait().flatten(2)
70 ctx.save_for_backward(local_output, weight)
71 ctx.state, ctx.has_bias = state, bias is not None
72 ctx.device_type = local_output.device.type
73 ctx.autocast_enabled = torch.is_autocast_enabled(ctx.device_type)
74 ctx.autocast_dtype = torch.get_autocast_dtype(ctx.device_type) if ctx.autocast_enabled else None
75 return F.linear(local_output, weight, bias) # pylint: disable=not-callable
76
77 @staticmethod
78 @once_differentiable
79 def backward(ctx: Any, gradient: torch.Tensor) -> tuple[torch.Tensor | None, ...]:
80 """Submit dO A2A before wgrad and wait before returning the FA gradient.
81
82 Args:
83 ctx: Saved operands, autocast settings and output layout.
84 gradient: Gradient of the projected hidden states.
85
86 Returns:
87 FA output, weight, bias and no state gradients.
88 """
89 with torch.autocast(ctx.device_type, enabled=ctx.autocast_enabled, dtype=ctx.autocast_dtype):
90 return _output_projection_backward(ctx, gradient)
91
92
93def _output_projection_backward(ctx, gradient):
94 """Keep the dO communication handle local to this backward invocation."""
95 local_output, weight = ctx.saved_tensors
96 state = ctx.state
97 needs_attention_grad, needs_weight_grad, needs_bias_grad = ctx.needs_input_grad[:3]
98 grad_output_a2a = None
99 if needs_attention_grad:
100 grad_local_output = gradient.matmul(weight).to(local_output.dtype)
101 grad_local_output = grad_local_output.reshape(*local_output.shape[:2], -1, state.head_dim)
102 grad_output_a2a = async_ulysses_seq_to_head_launch(grad_local_output, 1, 2, state.mesh)
103 flat_input = local_output.reshape(-1, local_output.shape[-1])
104 flat_gradient = gradient.reshape(-1, gradient.shape[-1])
105 grad_weight = None
106 if needs_weight_grad:
107 grad_weight = flat_input.transpose(0, 1).matmul(flat_gradient).transpose(0, 1)
108 grad_bias = flat_gradient.sum(0) if ctx.has_bias and needs_bias_grad else None
109 grad_attention_output = grad_output_a2a.wait() if grad_output_a2a is not None else None
110 return grad_attention_output, grad_weight, grad_bias, None
111
112
113def mla_output_projection_request(projection: nn.Linear) -> _ForwardRewriteRequest:
114 """Keep parameter materialization hooks while allowing dgrad/wgrad scheduling.
115
116 Args:
117 projection: Plain, unwrapped output Linear, before FSDP installation.
118
119 Returns:
120 An atomic child forward rewrite; ordinary calls remain unchanged.
121 """
122 # Quantized and LoRA subclasses need their own derivative contracts.
123 if type(projection) is not nn.Linear: # pylint: disable=unidiomatic-typecheck
124 raise ValueError("MLA backward overlap requires a plain output Linear")
125 if getattr(projection.forward, "__func__", None) is not nn.Linear.forward:
126 raise ValueError("MLA backward overlap requires an unwrapped plain output Linear")
127 original = projection.forward
128
129 @wraps(original)
130 def output_projection(*args: torch.Tensor, mla_output_state: MLAOutputProjectionState | None = None,
131 **kwargs: Any) -> torch.Tensor:
132 """Read managed parameters after their normal module pre-hooks.
133
134 Args:
135 *args: Positional arguments of the original Linear.
136 mla_output_state: Optional layout selecting the scheduled ordinary output.
137 **kwargs: Keyword arguments of the original Linear.
138
139 Returns:
140 Projected hidden states using ordinary or scheduled autograd.
141 """
142 if mla_output_state is None:
143 return original(*args, **kwargs)
144 # Scheduled calls consume the FA output; ordinary calls retain Linear input= support.
145 if len(args) != 1 or kwargs:
146 raise TypeError("Scheduled MLA output projection requires one positional activation")
147 return _OutputProjection.apply(args[0], projection.weight, projection.bias, mla_output_state)
148
149 return _ForwardRewriteRequest(projection, output_projection)
150
151
152@dataclass
153class MLAInputA2AState:
154 """Own one input A2A and its inverse for a single forward invocation.
155
156 Attributes:
157 mesh: Ulysses device mesh.
158 forward_a2a_handle: Q/K/V transfer from token-local to head-local layout.
159 backward_a2a_handle: Gradient transfer from head-local to token-local layout.
160 needs_send_grad: Whether the original input requires its inverse transfer.
161
162 Each handle owns the communication work and its buffers. The paired consumer
163 waits for it exactly once before reading the result, then clears the field.
164 """
165
166 mesh: Any
167 forward_a2a_handle: AsyncCPCollective | None = None
168 backward_a2a_handle: AsyncCPCollective | None = None
169 needs_send_grad: bool = False
170
171
172class _InputA2ALaunch(torch.autograd.Function):
173 """Keep independent native compute between a send and its gradient wait."""
174
175 @staticmethod
176 def forward(ctx: Any, send_tensor: torch.Tensor, compute_input: torch.Tensor,
177 state: MLAInputA2AState) -> torch.Tensor:
178 """Launch token-to-head A2A and attach the intervening compute edge.
179
180 Args:
181 ctx: Autograd context holding this invocation's A2A state.
182 send_tensor: Token-local tensor ready for communication.
183 compute_input: Input of the independent projection branch.
184 state: Forward and backward handles shared with the paired wait.
185
186 Returns:
187 A view of compute_input retaining both gradient dependencies.
188 """
189 ctx.state = state
190 state.needs_send_grad = ctx.needs_input_grad[0]
191 state.forward_a2a_handle = async_ulysses_seq_to_head_launch(send_tensor, 1, 2, state.mesh)
192 # Pass through the value without a copy while the paired nodes bracket
193 # the independent projection's native backward graph.
194 return compute_input.view_as(compute_input)
195
196 @staticmethod
197 @once_differentiable
198 def backward(ctx: Any, compute_gradient: torch.Tensor) -> tuple:
199 """Consume the inverse A2A after the independent branch backward.
200
201 Args:
202 ctx: Autograd context containing the paired A2A state.
203 compute_gradient: Gradient returned by the independent branch.
204
205 Returns:
206 Exchanged source gradient, compute gradient and no state gradient.
207 """
208 state = ctx.state
209 send_gradient = None
210 if state.needs_send_grad:
211 if state.backward_a2a_handle is None:
212 raise RuntimeError("MLA input consumer did not submit its inverse A2A")
213 send_gradient = state.backward_a2a_handle.wait()
214 state.backward_a2a_handle = None
215 return send_gradient, compute_gradient, None
216
217
218class _LatentInputA2AWait(torch.autograd.Function):
219 """Launch inverse Q before releasing the local KV projection gradients."""
220
221 @staticmethod
222 def forward(ctx: Any, key: torch.Tensor, value: torch.Tensor, state: MLAInputA2AState) -> tuple:
223 """Attach completed Q and both KV edges to the paired consumer.
224
225 Args:
226 ctx: Autograd context for the paired Q A2A.
227 key: Expanded head-local keys.
228 value: Expanded head-local values.
229 state: Q A2A submitted before the KV projection.
230
231 Returns:
232 Attention inputs in query, key, value order.
233 """
234 ctx.state = state
235 query = state.forward_a2a_handle.wait()
236 state.forward_a2a_handle = None
237 return query, key.view_as(key), value.view_as(value)
238
239 @staticmethod
240 @once_differentiable
241 def backward(ctx: Any, grad_query: torch.Tensor, grad_key: torch.Tensor,
242 grad_value: torch.Tensor) -> tuple:
243 """Run KV backward while dQ is being returned to its token owner.
244
245 Args:
246 ctx: Autograd context containing the paired Q A2A.
247 grad_query: Attention gradient for head-local queries.
248 grad_key: Attention gradient for head-local keys.
249 grad_value: Attention gradient for head-local values.
250
251 Returns:
252 Key and value gradients, followed by no state gradient.
253 """
254 state = ctx.state
255 if state.needs_send_grad:
256 state.backward_a2a_handle = async_ulysses_seq_to_head_launch(grad_query, 2, 1, state.mesh)
257 return grad_key, grad_value, None
258
259
260@dataclass
261class MLAExpandedInputA2AState:
262 """Own only the Q/K/V transfers for one expanded forward invocation."""
263
264 query: MLAInputA2AState
265 key: MLAInputA2AState
266 value: MLAInputA2AState
267
268 def wait(self) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
269 """Consume Q/K/V in attention order after all forward work is submitted."""
270 results = []
271 for a2a_state in (self.query, self.key, self.value):
272 results.append(a2a_state.forward_a2a_handle.wait())
273 a2a_state.forward_a2a_handle = None
274 return results[0], results[1], results[2]
275
276 def launch_backward(self, grad_query: torch.Tensor, grad_key: torch.Tensor,
277 grad_value: torch.Tensor) -> None:
278 """Submit each required inverse before releasing the independent compute.
279
280 Args:
281 grad_query: Head-local query gradient from attention.
282 grad_key: Head-local key gradient from attention.
283 grad_value: Head-local value gradient from attention.
284 """
285 for a2a_state, gradient in ((self.query, grad_query), (self.key, grad_key), (self.value, grad_value)):
286 if a2a_state.needs_send_grad:
287 a2a_state.backward_a2a_handle = async_ulysses_seq_to_head_launch(gradient, 2, 1, a2a_state.mesh)
288
289
290class _ExpandedInputA2AWait(torch.autograd.Function):
291 """Finish ordinary MLA inputs and return only Q's gradient to its projection."""
292
293 @staticmethod
294 def forward(ctx: Any, query: torch.Tensor, state: MLAExpandedInputA2AState) -> tuple:
295 """Submit Q after the shared V/K launches, then consume all three inputs.
296
297 Args:
298 ctx: Autograd context holding the three input transfers.
299 query: Final token-local query, with no following gate computation.
300 state: Per-forward Q/K/V communication state.
301
302 Returns:
303 Completed attention inputs in query, key, value order.
304 """
305 ctx.state = state
306 state.query.needs_send_grad = ctx.needs_input_grad[0]
307 state.query.forward_a2a_handle = async_ulysses_seq_to_head_launch(query, 1, 2, state.query.mesh)
308 return state.wait()
309
310 @staticmethod
311 @once_differentiable
312 def backward(ctx: Any, grad_query: torch.Tensor, grad_key: torch.Tensor,
313 grad_value: torch.Tensor) -> tuple:
314 """Submit dQ/dK/dV and release Q backward after dQ has arrived.
315
316 Args:
317 ctx: Autograd context holding the three paired input transfers.
318 grad_query: Attention gradient for head-local queries.
319 grad_key: Attention gradient for head-local keys.
320 grad_value: Attention gradient for head-local values.
321
322 Returns:
323 Token-local Q gradient and no communication-state gradient.
324 """
325 state = ctx.state
326 state.launch_backward(grad_query, grad_key, grad_value)
327 query_gradient = None
328 if state.query.needs_send_grad:
329 query_gradient = state.query.backward_a2a_handle.wait()
330 state.query.backward_a2a_handle = None
331 return query_gradient, None
332
333
334def launch_input_a2a(send_tensor: torch.Tensor, compute_input: torch.Tensor,
335 a2a_state: MLAInputA2AState) -> torch.Tensor:
336 """Submit an input A2A while retaining its native collective backward.
337
338 Args:
339 send_tensor: Final token-local Q, K or V ready to send.
340 compute_input: Input of the independent branch to execute next.
341 a2a_state: State receiving this forward communication handle.
342
343 Returns:
344 Unchanged compute_input, without adding a paired autograd dependency.
345 """
346 a2a_state.forward_a2a_handle = async_ulysses_seq_to_head_launch(send_tensor, 1, 2, a2a_state.mesh)
347 return compute_input
348
349
350def finish_expanded_inputs(query: torch.Tensor, state: MLAExpandedInputA2AState,
351 overlap_backward: bool) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
352 """Finish ungated expanded inputs using paired or native collective gradients.
353
354 Args:
355 query: Final token-local query, after V and K have been submitted.
356 state: This invocation's Q/K/V communication state.
357 overlap_backward: Pair input autograd nodes for the pure-Ulysses backward.
358
359 Returns:
360 Completed query, key and value tensors in head-local layout.
361 """
362 if overlap_backward:
363 return _ExpandedInputA2AWait.apply(query, state)
364 state.query.forward_a2a_handle = async_ulysses_seq_to_head_launch(query, 1, 2, state.query.mesh)
365 return state.wait()
366
367
368class _LatentKVLinear(torch.autograd.Function):
369 """Schedule the gathered latent adjoint before selected-head weight gradients."""
370
371 @staticmethod
372 def forward(ctx: Any, payload: torch.Tensor, weight: torch.Tensor, bias: torch.Tensor | None,
373 gathered: torch.Tensor, mesh: Any) -> tuple:
374 """Reuse the completed gather inside the managed Linear module.
375
376 Args:
377 ctx: Autograd state for this projection.
378 payload: Local payload supplying the single gather gradient edge.
379 weight: Selected head rows of the managed projection weight.
380 bias: Optional selected projection bias.
381 gathered: Completed full-sequence latent and shared-key payload.
382 mesh: Chronological CP mesh used by the forward gather.
383
384 Returns:
385 Expanded KV heads and the shared key, without duplicating its gradient.
386 """
387 del payload
388 latent = gathered[..., :weight.shape[-1]].contiguous()
389 shared_key = gathered[..., weight.shape[-1]:]
390 ctx.save_for_backward(latent, weight)
391 ctx.mesh, ctx.has_bias = mesh, bias is not None
392 ctx.device_type = latent.device.type
393 ctx.autocast_enabled = torch.is_autocast_enabled(ctx.device_type)
394 ctx.autocast_dtype = torch.get_autocast_dtype(ctx.device_type) if ctx.autocast_enabled else None
395 return F.linear(latent, weight, bias), shared_key.view_as(shared_key) # pylint: disable=not-callable
396
397 @staticmethod
398 @once_differentiable
399 def backward(ctx: Any, gradient: torch.Tensor, shared_gradient: torch.Tensor) -> tuple:
400 """Overlap SUM ReduceScatter with the independent KV weight gradient.
401
402 Args:
403 ctx: Saved projection operands, mesh and autocast state.
404 gradient: Gradient of the expanded head-local KV features.
405 shared_gradient: Gradient of the gathered shared key features.
406
407 Returns:
408 Local payload, weight and optional bias gradients; no gather or mesh gradient.
409 """
410 with torch.autocast(ctx.device_type, enabled=ctx.autocast_enabled, dtype=ctx.autocast_dtype):
411 return _latent_projection_backward(ctx, gradient, shared_gradient)
412
413
414def _latent_projection_backward(ctx, gradient, shared_gradient):
415 """Keep both reduction buffers alive until their local gradient is consumed."""
416 activation, weight = ctx.saved_tensors
417 needs_payload_grad, needs_weight_grad, needs_bias_grad = ctx.needs_input_grad[:3]
418 work = None
419 if needs_payload_grad:
420 grad_latent = gradient.matmul(weight).to(activation.dtype)
421 grad_payload = torch.cat((grad_latent, shared_gradient), dim=-1)
422 send = grad_payload.movedim(1, 0).contiguous()
423 shape = list(send.shape)
424 shape[0] //= ctx.mesh.size()
425 receive = send.new_empty(shape)
426 work = dist.reduce_scatter_tensor(
427 receive, send, op=dist.ReduceOp.SUM, group=ctx.mesh.get_group(), async_op=True,
428 )
429 flat_gradient = gradient.reshape(-1, gradient.shape[-1])
430 grad_weight = None
431 if needs_weight_grad:
432 flat_activation = activation.reshape(-1, activation.shape[-1])
433 grad_weight = flat_gradient.transpose(0, 1).matmul(flat_activation)
434 grad_bias = flat_gradient.sum(0) if ctx.has_bias and needs_bias_grad else None
435 local = None
436 if work is not None:
437 work.wait()
438 local = receive.movedim(0, 1).contiguous()
439 return local, grad_weight, grad_bias, None, None