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"""KDA state exchange through AllGather or grouped AllGather/P2P."""
16# pylint: disable=forbidden-backend-import
17from __future__ import annotations
18
19from typing import Any
20
21import torch
22import torch.distributed as dist
23
24from hyper_parallel.core.dtensor.device_mesh import DeviceMesh
25
26
27def _validate_mesh_order(mesh: DeviceMesh) -> None:
28 """Prevent collective slots from being mistaken for chronological positions."""
29 group_ranks = tuple(dist.get_process_group_ranks(mesh.get_group()))
30 if tuple(mesh.rank_list) != group_ranks:
31 raise ValueError("KDA mesh sequence order must match its communication group order.")
32
33
34def split_kda_mesh(mesh: DeviceMesh, degree: int, names: tuple[str, str]) -> DeviceMesh:
35 """Split a CP axis without losing the root rank map or sibling DP groups.
36
37 Args:
38 mesh: One-dimensional chronological CP mesh.
39 degree: Size of the inner split axis.
40 names: Names of the outer and inner axes.
41
42 Returns:
43 Two-dimensional submesh attached to the original root.
44 """
45 if mesh.ndim != 1 or not isinstance(degree, int) or isinstance(degree, bool):
46 raise ValueError("KDA mesh splitting requires a 1-D mesh and integer degree.")
47 if degree < 1 or mesh.size() % degree:
48 raise ValueError("KDA split degree must be positive and divide the CP size.")
49 _validate_mesh_order(mesh)
50 # Group creation uses WORLD; every rank must derive the same sibling groups.
51 root = mesh._get_root_mesh() # pylint: disable=protected-access
52 if root.size() != dist.get_world_size():
53 raise ValueError("Hybrid KDA requires a CP submesh retaining its WORLD root mesh.")
54 # Like the existing hybrid CP helper, preserve all sibling groups through the root layout.
55 return mesh._unflatten(0, (mesh.size() // degree, degree), names) # pylint: disable=protected-access
56
57
58def build_kda_state_cp(mesh: DeviceMesh, protocol: str, allgather_group_size: int) -> object | None:
59 """Construct a state-CP executor holding only communication metadata.
60
61 Args:
62 mesh: One-dimensional mesh ordered along the state sequence.
63 protocol: p2p, allgather, or grouped_allgather_p2p.
64 allgather_group_size: Consecutive ranks per local gather, only for the grouped protocol.
65
66 Returns:
67 Gather executor, or None to select the existing P2P path.
68 """
69 if protocol not in ("p2p", "allgather", "grouped_allgather_p2p"):
70 raise ValueError(f"Unknown KDA state_cp_method: {protocol!r}.")
71 if not isinstance(allgather_group_size, int) or isinstance(allgather_group_size, bool) or allgather_group_size < 1:
72 raise ValueError("KDA allgather_group_size must be a positive integer.")
73 if protocol != "grouped_allgather_p2p" and allgather_group_size != 1:
74 raise ValueError("KDA allgather_group_size is only valid for grouped_allgather_p2p.")
75 if protocol == "p2p":
76 return None
77 ranks = tuple(mesh.rank_list)
78 rank, size = mesh.get_local_rank(), mesh.size()
79 if protocol == "allgather":
80 _validate_mesh_order(mesh)
81 return KDAStateAllGather(mesh.get_group(), rank, size)
82 if size % allgather_group_size:
83 raise ValueError("KDA allgather_group_size must divide the state CP size.")
84 if allgather_group_size == 1:
85 return None
86 if allgather_group_size == size:
87 _validate_mesh_order(mesh)
88 return KDAStateAllGather(mesh.get_group(), rank, size)
89 split = split_kda_mesh(mesh, allgather_group_size, ("kda_outer", "kda_group"))
90 return KDAStateAllGatherP2P(
91 mesh.get_group(), split["kda_group"].get_group(), rank, ranks, allgather_group_size)
92
93
94def _validate_summary(state: torch.Tensor, matrix: torch.Tensor) -> None:
95 """Validate the dense affine contract before entering communication."""
96 if (state.ndim != 4 or state.shape[-2:] != (128, 128) or matrix.shape != state.shape
97 or state.shape[0] == 0 or state.shape[1] == 0):
98 raise ValueError("KDA state CP requires nonempty matching [B,H,128,128] summaries.")
99 if state.dtype != torch.float32 or matrix.dtype != torch.float32 or state.device != matrix.device:
100 raise ValueError("KDA state CP requires FP32 summaries on the same device.")
101 if state.device.type not in ("cpu", "npu"):
102 raise ValueError("KDA state CP supports CPU protocol checks and Ascend NPU execution only.")
103
104
105def merge_affine_summaries(
106 states: torch.Tensor, matrices: torch.Tensor, rank: int, forward: bool,
107) -> torch.Tensor:
108 """Merge the exclusive prefix or transposed suffix in chronological order.
109
110 Args:
111 states: Affine offsets shaped [P,B,H,128,128].
112 matrices: Matching transfer matrices, possibly views of packed storage.
113 rank: Chronological rank whose input state or final-state gradient is requested.
114 forward: Select forward prefix instead of backward suffix.
115
116 Returns:
117 Contiguous FP32 recurrent state or state gradient shaped [B,H,128,128].
118 """
119 if states.ndim != 5 or states.shape != matrices.shape or states.shape[0] == 0:
120 raise ValueError("KDA merge requires matching rank-stacked summaries.")
121 size, batch, heads = states.shape[:3]
122 _validate_summary(states[0], matrices[0])
123 if not 0 <= rank < size:
124 raise ValueError("KDA merge rank is outside the summary group.")
125 if any(t.stride(1) != heads * t.stride(2) or t.stride(-1) != 1 for t in (states, matrices)):
126 raise ValueError("KDA merge requires consecutive batch/head slabs and columns.")
127 if states.device.type != "npu":
128 raise ValueError("KDA fused state merge requires Ascend NPU tensors.")
129 count = rank if forward else size - rank - 1
130 output = states.new_empty((batch, heads, 128, 128))
131 if count == 0:
132 return output.zero_()
133 # Triton is optional for imports; production merging requires Ascend.
134 # pylint: disable=import-outside-toplevel
135 from hyper_parallel.components.functional._triton.kimi_delta_attention.cp_merge import (
136 kda_cached_affine_merge_kernel,
137 )
138 # pylint: enable=import-outside-toplevel
139
140 kda_cached_affine_merge_kernel[(2, batch * heads)](
141 output, states, matrices, count, rank,
142 state_rank_stride=states.stride(0), state_head_stride=states.stride(2), state_row_stride=states.stride(3),
143 matrix_rank_stride=matrices.stride(0), matrix_head_stride=matrices.stride(2),
144 matrix_row_stride=matrices.stride(3),
145 forward=forward, block_v=64, num_warps=2, num_stages=2)
146 return output
147
148
149def _gather_maps(state: torch.Tensor, matrix: torch.Tensor, group: Any, size: int) -> torch.Tensor:
150 """Pack validated local summaries for one invocation; the caller owns the gathered storage."""
151 packed = torch.cat((state, matrix), dim=-1)
152 output = packed.new_empty((size * packed.shape[0], *packed.shape[1:]))
153 dist.all_gather_into_tensor(output, packed, group=group)
154 return output.view(size, *packed.shape)
155
156
157class KDAStateAllGather:
158 """Gather S/M forward and G/M backward; only local M survives forward."""
159
160 def __init__(self, group: Any, rank: int, size: int) -> None:
161 """Bind precreated communication metadata, without tensor workspaces."""
162 if not isinstance(size, int) or isinstance(size, bool) or size < 1 or not 0 <= rank < size:
163 raise ValueError("AllGather requires a positive size and valid local rank.")
164 self.group, self.rank, self.size = group, rank, size
165
166 def forward(self, state: torch.Tensor, matrix: torch.Tensor) -> torch.Tensor:
167 """Return the incoming state; the caller saves its original local M.
168
169 Args:
170 state: Local FP32 offset S [B,H,128,128].
171 matrix: Local forward transfer M of the same shape.
172
173 Returns:
174 This rank's incoming recurrent state.
175 """
176 _validate_summary(state, matrix)
177 maps = _gather_maps(state, matrix, self.group, self.size)
178 return merge_affine_summaries(maps[..., :128], maps[..., 128:], self.rank, True)
179
180 def backward(self, gradient: torch.Tensor, matrix: torch.Tensor) -> torch.Tensor:
181 """Collect new G with the saved local M and recover the outgoing gradient.
182
183 Args:
184 gradient: Local FP32 offset G [B,H,128,128].
185 matrix: This invocation's cached local forward M.
186
187 Returns:
188 This rank's outgoing recurrent-state gradient.
189 """
190 _validate_summary(gradient, matrix)
191 maps = _gather_maps(gradient, matrix, self.group, self.size)
192 return merge_affine_summaries(maps[..., :128], maps[..., 128:], self.rank, False)
193
194
195def _validate_scan_range(indices: range, size: int, forward: bool, prefix_count: int | None) -> None:
196 """Check the requested chronological interval and optional saved prefix."""
197 count = len(indices)
198 if prefix_count is not None and not 0 <= prefix_count <= count:
199 raise ValueError("The saved prefix must lie within the scan interval.")
200 if count:
201 endpoints_valid = all(0 <= index < size for index in (indices[0], indices[-1]))
202 if indices.step != (1 if forward else -1) or not endpoints_valid:
203 raise ValueError("KDA scans require an ordered interval inside the group.")
204
205
206def _scan_maps(maps: torch.Tensor, incoming: torch.Tensor,
207 indices: range, forward: bool, prefix_count: int | None = None,
208 ) -> tuple[torch.Tensor, torch.Tensor | None]:
209 """Scan validated group maps once, optionally returning an intermediate prefix."""
210 count = len(indices)
211 _validate_scan_range(indices, maps.shape[0], forward, prefix_count)
212 if not count:
213 return incoming, incoming if prefix_count == 0 else None
214 if incoming.device.type != "npu" or not incoming.is_contiguous():
215 raise ValueError("KDA fused scans require a contiguous Ascend input state.")
216 output = torch.empty_like(incoming)
217 prefix = None
218 if prefix_count is not None:
219 prefix = incoming if prefix_count == 0 else output if prefix_count == count else torch.empty_like(incoming)
220 save_prefix = prefix is not None and prefix_count not in (0, count)
221 # Keep the optional Triton dependency out of CPU-only model imports.
222 # pylint: disable=import-outside-toplevel
223 from hyper_parallel.components.functional._triton.kimi_delta_attention.cp_merge import kda_affine_scan_kernel
224 # pylint: enable=import-outside-toplevel
225
226 batch, heads = incoming.shape[:2]
227 kda_affine_scan_kernel[(2, batch * heads)](
228 output, prefix, maps, incoming, count, indices.start, prefix_count or 0,
229 rank_stride=maps.stride(0), head_stride=maps.stride(2), row_stride=maps.stride(3),
230 forward=forward, save_prefix=save_prefix, block_v=64, num_warps=2, num_stages=2)
231 return output, prefix
232
233
234class KDAStateAllGatherP2P:
235 """Use group-local gathers and an owner chain, retaining only each local M."""
236
237 def __init__(self, group: Any, intra_group: Any, rank: int,
238 ranks: tuple[int, ...], width: int) -> None:
239 """Bind ranks already validated against the process-group order by the factory."""
240 size = len(ranks)
241 if not isinstance(width, int) or isinstance(width, bool) or width < 2 or size % width:
242 raise ValueError("Grouped AllGather width must be >= 2 and divide the CP size.")
243 if not 0 <= rank < size or len(set(ranks)) != size:
244 raise ValueError("Grouped AllGather requires unique global ranks and a valid local rank.")
245 self.group, self.intra, self.rank, self.width = group, intra_group, rank, width
246 self.ranks = tuple(ranks)
247 self.block, self.local = divmod(rank, width)
248 self.blocks = size // width
249 self.owner = self.block * width + width - 1
250
251 def _receive(self, state: torch.Tensor, forward: bool) -> tuple[torch.Tensor, Any]:
252 """Post the owner receive before gathering local summaries."""
253 incoming = torch.zeros_like(state)
254 recv_handle = None
255 peer = self.owner - self.width if forward else self.owner + self.width
256 if self.rank == self.owner and 0 <= peer < len(self.ranks):
257 recv_handle = dist.irecv(incoming, src=self.ranks[peer], group=self.group)
258 return incoming, recv_handle
259
260 def forward(self, state: torch.Tensor, matrix: torch.Tensor) -> torch.Tensor:
261 """Relay ordered group scans and restore this rank's incoming state.
262
263 Args:
264 state: Local FP32 offset S [B,H,128,128].
265 matrix: Local forward transfer M of the same shape.
266
267 Returns:
268 This rank's incoming recurrent state.
269 """
270 _validate_summary(state, matrix)
271 incoming, recv_handle = self._receive(state, True)
272 maps = _gather_maps(state, matrix, self.intra, self.width)
273 send_handle, send_state = None, None
274 result = None
275 if self.rank == self.owner:
276 if recv_handle is not None:
277 recv_handle.wait()
278 if self.block + 1 < self.blocks:
279 send_state, result = _scan_maps(maps, incoming, range(self.width), True,
280 prefix_count=self.local)
281 send_handle = dist.isend(send_state, dst=self.ranks[self.owner + self.width], group=self.group)
282 if self.block > 0:
283 dist.broadcast(incoming, src=self.ranks[self.owner], group=self.intra)
284 if result is None:
285 result, _ = _scan_maps(maps, incoming, range(self.local), True)
286 if send_handle is not None:
287 send_handle.wait()
288 return result
289
290 def backward(self, gradient: torch.Tensor, matrix: torch.Tensor) -> torch.Tensor:
291 """Regather G/M and reverse the owner chain with ordered state feedback.
292
293 Args:
294 gradient: Local FP32 offset G [B,H,128,128].
295 matrix: This invocation's cached local forward M.
296
297 Returns:
298 This rank's outgoing recurrent-state gradient.
299 """
300 _validate_summary(gradient, matrix)
301 incoming, recv_handle = self._receive(gradient, False)
302 maps = _gather_maps(gradient, matrix, self.intra, self.width)
303 send_handle, send_state = None, None
304 if self.rank == self.owner:
305 if recv_handle is not None:
306 recv_handle.wait()
307 if self.block > 0:
308 send_state, _ = _scan_maps(maps, incoming, range(self.width - 1, -1, -1), False)
309 send_handle = dist.isend(send_state, dst=self.ranks[self.owner - self.width], group=self.group)
310 if self.block + 1 < self.blocks:
311 dist.broadcast(incoming, src=self.ranks[self.owner], group=self.intra)
312 result, _ = _scan_maps(maps, incoming, range(self.width - 1, self.local, -1), False)
313 if send_handle is not None:
314 send_handle.wait()
315 return result