Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_multicore / modules / mega_moe / route.py: 95%
353 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 05:03 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 05:03 +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"""Prepare trusted Router output for managed Torch MegaMoe execution."""
17from __future__ import annotations
19from array import array
20from collections.abc import Callable
21from dataclasses import dataclass, replace
22from itertools import accumulate
23from typing import TYPE_CHECKING, Any
25import torch
26import torch.distributed as dist
28from hyper_multicore._loader import load_torch_npu
29from hyper_multicore.torch import ops as multicore_ops
31from .spec import MegaMoeSpec, _align_capacity
33if TYPE_CHECKING:
34 from .workspace import MegaMoeWorkspace
37# Avoid large Host transfers and Python boxing when the gathered count table grows.
38_HOST_ROUTE_MAX_COUNTS = 2048
39# Preserve the smaller descriptor budget when counts change on every call.
40_HOST_DESCRIPTOR_MAX_COUNTS = 1024
41_ROUTE_CACHE_REPROBE_STEPS = 8
42_METADATA_ALIGNMENT = 512
43_PULL_STAGING_MAX_BYTES = 64 * 1024 * 1024
46@dataclass(frozen=True)
47class RouteMetadata:
48 """Exact offsets and counts consumed by the low-level native ABI."""
50 dispatch_src_off: torch.Tensor
51 dispatch_target_off: torch.Tensor
52 dispatch_size: torch.Tensor
53 combine_src_off: torch.Tensor
54 combine_target_off: torch.Tensor
55 combine_size: torch.Tensor
56 group_list: torch.Tensor
57 expert_capacity: int
58 local_num_tokens: int | None = None
59 plan_tokens: int | None = None
62@dataclass(frozen=True)
63class PreparedTopKRoute:
64 """Permuted payload and exact metadata for one Router decision."""
66 routed_tokens: torch.Tensor
67 unpermute_mapping: torch.Tensor
68 tokens_per_expert: torch.Tensor
69 received_counts: torch.Tensor
70 metadata: RouteMetadata
71 maximum_received_slots: int = 0
74@dataclass(frozen=True)
75class _HostRouteCache:
76 """One validated wire table and immutable route descriptors for a workspace."""
78 signature: tuple[Any, ...]
79 raw: list[list[int]]
80 summary: tuple[int, int, int]
81 metadata: RouteMetadata | None
82 stream: Any = None
83 ready: Any = None
85 def wait(self, device: Any) -> None:
86 """Order a different consumer stream after the descriptor upload.
88 Args:
89 device: Device whose current stream consumes the cached descriptors.
90 """
91 if self.ready is not None and self.metadata is not None:
92 current = torch.npu.current_stream(device)
93 if current != self.stream:
94 current.wait_event(self.ready)
95 for tensor in vars(self.metadata).values():
96 if isinstance(tensor, torch.Tensor):
97 tensor.record_stream(current)
100@dataclass(frozen=True)
101class _RouteHeaderCache:
102 """One immutable control header, retained across identical local invocations."""
104 key: tuple[Any, ...]
105 tensor: torch.Tensor | None
106 stream: Any = None
107 ready: Any = None
109 def wait(self, device: Any) -> None:
110 """Preserve upload ordering and allocation lifetime on another stream.
112 Args:
113 device: Device whose current stream consumes the cached header.
114 """
115 if self.ready is not None and self.tensor is not None:
116 current = torch.npu.current_stream(device)
117 if current != self.stream:
118 current.wait_event(self.ready)
119 self.tensor.record_stream(current)
122def _validate_topk_inputs(
123 hidden_states: torch.Tensor,
124 topk_ids: torch.Tensor,
125 topk_weights: torch.Tensor,
126) -> torch.Tensor:
127 """Validate route tensor metadata without synchronizing tensor contents."""
128 if topk_ids.dtype not in (torch.int32, torch.int64):
129 raise TypeError(f"topk_ids must be int32 or int64, got {topk_ids.dtype}.")
130 if not topk_weights.is_floating_point():
131 raise TypeError(
132 f"topk_weights must be floating point, got {topk_weights.dtype}."
133 )
134 if (
135 topk_ids.device != hidden_states.device
136 or topk_weights.device != hidden_states.device
137 ):
138 raise ValueError(
139 "hidden_states, topk_ids and topk_weights must be on one device."
140 )
141 return topk_ids.reshape(-1)
144def _resolve_counts(
145 flat_ids: torch.Tensor,
146 supplied_counts: torch.Tensor | None,
147 spec: MegaMoeSpec,
148) -> torch.Tensor:
149 """Use trusted Router counts or compute them only when omitted."""
150 if supplied_counts is None:
151 return (
152 torch.bincount(flat_ids.to(torch.int64), minlength=spec.num_experts)
153 .to(torch.int32)
154 .contiguous()
155 )
156 if tuple(supplied_counts.shape) != (spec.num_experts,):
157 raise ValueError(
158 f"tokens_per_expert must have shape ({spec.num_experts},), "
159 f"got {tuple(supplied_counts.shape)}."
160 )
161 if supplied_counts.device != flat_ids.device:
162 raise ValueError("tokens_per_expert must be on the route tensor device.")
163 if supplied_counts.dtype not in (torch.int32, torch.int64):
164 raise TypeError(
165 f"tokens_per_expert must be int32 or int64, got {supplied_counts.dtype}."
166 )
167 if supplied_counts.dtype == torch.int32 and supplied_counts.is_contiguous():
168 return supplied_counts
169 return supplied_counts.to(torch.int32).contiguous()
172def _start_count_gather(
173 counts: torch.Tensor,
174 spec: MegaMoeSpec,
175) -> tuple[torch.Tensor, Any | None]:
176 """Start the rank-major INT32 count gather used by route construction."""
177 if spec.ep_size == 1:
178 return counts.reshape(1, spec.num_experts), None
179 gathered_counts = torch.empty(
180 spec.ep_size * spec.num_experts,
181 dtype=torch.int32,
182 device=counts.device,
183 )
184 work = dist.all_gather_into_tensor(
185 gathered_counts,
186 counts,
187 group=spec.ep_group,
188 async_op=True,
189 )
190 return gathered_counts.reshape(spec.ep_size, spec.num_experts), work
193def _finish_count_gather(
194 counts_by_source: torch.Tensor,
195 work: Any | None,
196 spec: MegaMoeSpec,
197) -> torch.Tensor:
198 """Wait for gathered counts before reading route metadata."""
199 if work is not None:
200 work.wait()
201 local_start = spec.rank_id * spec.local_experts
202 local_end = local_start + spec.local_experts
203 return counts_by_source[:, local_start:local_end].contiguous()
206def _expert_capacity(
207 counts_by_source: torch.Tensor,
208 spec: MegaMoeSpec,
209) -> tuple[int, int]:
210 """Validate the global bound and size rank-local computation tensors."""
211 destination_loads = counts_by_source.reshape(
212 spec.ep_size,
213 spec.ep_size,
214 spec.local_experts,
215 ).sum(dim=(0, 2), dtype=torch.int32)
216 # One host transfer serves both the coordinated overflow check and local
217 # allocation; the existing gathered counts require no extra collective.
218 loads = destination_loads.tolist()
219 # Keep a non-null ABI argument on ranks whose experts receive no tokens.
220 # Source outputs and symmetric communication buffers retain their own sizes.
221 return max(1, loads[spec.rank_id]), max(loads)
224def _permute_topk_input(
225 hidden_states: torch.Tensor,
226 topk_ids: torch.Tensor,
227) -> tuple[torch.Tensor, torch.Tensor]:
228 """Expand hidden rows with the native NPU MoE permutation operator."""
229 if hidden_states.shape[0] == 0:
230 return hidden_states, torch.empty(0, dtype=torch.int32, device=hidden_states.device)
231 routed_tokens, unpermute_mapping = load_torch_npu().npu_moe_token_permute(
232 hidden_states,
233 topk_ids,
234 )
235 return (
236 routed_tokens,
237 unpermute_mapping.reshape(-1).to(torch.int32).contiguous(),
238 )
241def _permute_topk_input_out(
242 hidden_states: torch.Tensor,
243 topk_ids: torch.Tensor,
244 output: torch.Tensor,
245) -> tuple[torch.Tensor, torch.Tensor]:
246 """Keep the inverse mapping owned while writing rows into leased SHMEM."""
247 mapping = torch.empty(topk_ids.numel(), dtype=torch.int32, device=topk_ids.device)
248 multicore_ops.moe_token_permute_out(hidden_states.contiguous(), topk_ids.contiguous(), output, mapping)
249 return output, mapping
252def _compute_route_metadata(
253 counts: torch.Tensor,
254 counts_by_source: torch.Tensor,
255 received_counts: torch.Tensor,
256 spec: MegaMoeSpec,
257 expert_capacity: int,
258) -> RouteMetadata:
259 """Build exact element offsets without padding or route truncation."""
260 counts_i32 = counts.to(dtype=torch.int32).contiguous()
261 source_prefix = counts_by_source.cumsum(dim=1, dtype=torch.int64) - counts_by_source
262 destination_layout = (
263 counts_by_source.reshape(
264 spec.ep_size,
265 spec.ep_size,
266 spec.local_experts,
267 )
268 .permute(1, 2, 0)
269 .contiguous()
270 )
271 destination_flat = destination_layout.reshape(spec.ep_size, -1)
272 destination_prefix = (
273 destination_flat.cumsum(dim=1, dtype=torch.int64) - destination_flat
274 ).reshape(spec.ep_size, spec.local_experts, spec.ep_size)
275 local_destination_prefix = destination_prefix[spec.rank_id]
276 local_destination_counts = destination_layout[spec.rank_id]
277 local_start = spec.rank_id * spec.local_experts
278 local_end = local_start + spec.local_experts
279 return RouteMetadata(
280 dispatch_src_off=(source_prefix[:, local_start:local_end].reshape(-1).to(torch.int64)
281 if spec.dispatch_mode == "pull" else source_prefix[spec.rank_id].to(torch.int64)),
282 dispatch_target_off=(
283 local_destination_prefix.T.reshape(-1).to(torch.int64) if spec.dispatch_mode == "pull"
284 else destination_prefix[:, :, spec.rank_id].reshape(-1).to(torch.int64)
285 ),
286 dispatch_size=(received_counts.reshape(-1).to(torch.int32) if spec.dispatch_mode == "pull" else counts_i32),
287 combine_src_off=local_destination_prefix.T.reshape(-1).to(torch.int64),
288 combine_target_off=(
289 source_prefix[:, local_start:local_end].reshape(-1).to(torch.int64)
290 ),
291 combine_size=received_counts.reshape(-1).to(torch.int32),
292 group_list=(
293 local_destination_prefix[:, -1] + local_destination_counts[:, -1]
294 ).to(torch.int64),
295 expert_capacity=expert_capacity,
296 )
299def _upload_route_tensor(tensor: torch.Tensor, device: Any) -> torch.Tensor:
300 """Keep Host metadata alive through the pinned allocator during asynchronous upload."""
301 if torch.device(device).type == "npu":
302 return tensor.pin_memory().to(device, non_blocking=True)
303 return tensor.to(device)
306def _host_route_metadata(
307 raw: list[list[int]],
308 spec: MegaMoeSpec,
309 capacity: int,
310 device: Any,
311) -> RouteMetadata:
312 """Pack the native INT64 offsets and INT32 route sizes in one retained allocation."""
313 counts = [row[3:] for row in raw]
314 local, rank = spec.local_experts, spec.rank_id
315 start = rank * local
316 prefix, destinations, destination_prefix = _host_route_prefixes(counts, spec)
317 source_offsets = [
318 prefix[source][expert] for source in range(spec.ep_size) for expert in range(start, start + local)
319 ]
320 target_offsets = [
321 destination_prefix[rank][expert * spec.ep_size + source]
322 for source in range(spec.ep_size)
323 for expert in range(local)
324 ]
325 received = [counts[source][expert] for source in range(spec.ep_size) for expert in range(start, start + local)]
326 if spec.dispatch_mode == "pull":
327 dispatch_source, dispatch_target, dispatch_sizes = source_offsets, target_offsets, received
328 else:
329 dispatch_source = prefix[rank]
330 dispatch_target = [
331 destination_prefix[target][expert * spec.ep_size + rank]
332 for target in range(spec.ep_size)
333 for expert in range(local)
334 ]
335 dispatch_sizes = counts[rank]
336 groups = [
337 destination_prefix[rank][(expert + 1) * spec.ep_size - 1] + destinations[rank][(expert + 1) * spec.ep_size - 1]
338 for expert in range(local)
339 ]
340 wide = (dispatch_source, dispatch_target, target_offsets, source_offsets, groups)
341 return _pack_host_route(wide, (dispatch_sizes, received), capacity, device)
344def _host_route_prefixes(
345 counts: list[list[int]], spec: MegaMoeSpec,
346) -> tuple[list[list[int]], list[list[int]], list[list[int]]]:
347 """Scan each source and each destination independently in native layout order."""
348 local = spec.local_experts
349 prefix = [list(accumulate(row, initial=0))[:-1] for row in counts]
350 destinations = [
351 [
352 counts[source][expert]
353 for expert in range(target * local, (target + 1) * local)
354 for source in range(spec.ep_size)
355 ]
356 for target in range(spec.ep_size)
357 ]
358 destination_prefix = [list(accumulate(row, initial=0))[:-1] for row in destinations]
359 return prefix, destinations, destination_prefix
362def _pack_host_route(
363 wide: tuple[list[int], ...],
364 size_fields: tuple[list[int], ...],
365 capacity: int,
366 device: Any,
367) -> RouteMetadata:
368 """Transfer aligned descriptor fields together and retain storage through their views."""
369 fields = [*wide, *size_fields]
370 dtypes = [torch.int64] * len(wide) + [torch.int32] * 2
371 storage = bytearray()
372 slices = []
373 for values, dtype in zip(fields, dtypes):
374 encoded = array("q" if dtype == torch.int64 else "i", values).tobytes()
375 start = len(storage)
376 storage.extend(encoded)
377 slices.append((start, len(storage), dtype))
378 storage.extend(bytes((-len(storage)) % _METADATA_ALIGNMENT))
379 buffer = _upload_route_tensor(torch.frombuffer(storage, dtype=torch.uint8), device)
380 tensors = [buffer[start:end].view(dtype) for start, end, dtype in slices]
381 offsets, sizes = tensors[:5], tensors[5:]
382 return RouteMetadata(
383 dispatch_src_off=offsets[0],
384 dispatch_target_off=offsets[1],
385 dispatch_size=sizes[0],
386 combine_src_off=offsets[2],
387 combine_target_off=offsets[3],
388 combine_size=sizes[1],
389 group_list=offsets[4],
390 expert_capacity=capacity,
391 )
394def prepare_topk_route(
395 hidden_states: torch.Tensor,
396 topk_ids: torch.Tensor,
397 topk_weights: torch.Tensor,
398 spec: MegaMoeSpec,
399 tokens_per_expert: torch.Tensor | None,
400 workspace: MegaMoeWorkspace | None = None,
401 *,
402 autograd_mask: int = 0,
403 prepare_workspace: Callable[[int, int], None] | None = None,
404) -> PreparedTopKRoute:
405 """Overlap Top-K permutation with count exchange and build route metadata.
407 Args:
408 hidden_states: Flattened token states to route.
409 topk_ids: Global experts selected for every token.
410 topk_weights: Router weights paired with ``topk_ids``.
411 spec: Bound shape and expert-parallel specification.
412 tokens_per_expert: Optional trusted Router histogram.
413 workspace: Optional workspace ordered before the count exchange. Pull
414 requires a lease acquired by the caller or the automatic-capacity callback.
415 autograd_mask: Group-consistent gradient participation flags in dynamic mode.
416 prepare_workspace: Automatic-capacity callback, run after count agreement and before pull permutation.
418 Returns:
419 Permuted tokens and exact native route metadata.
420 """
421 flat_ids = _validate_topk_inputs(hidden_states, topk_ids, topk_weights)
422 if spec.dynamic_tokens:
423 return _prepare_dynamic_route(hidden_states, topk_ids, flat_ids, spec, tokens_per_expert,
424 workspace, autograd_mask, prepare_workspace)
425 counts = _resolve_counts(flat_ids, tokens_per_expert, spec)
426 permutation_output = None
427 if workspace is not None:
428 if spec.dispatch_mode == "pull":
429 if not workspace.in_use or workspace.source_buffer is None:
430 raise RuntimeError("Pull permutation requires an initialized, claimed workspace.")
431 permutation_output = workspace.source_buffer
432 else:
433 workspace.wait_for_reuse()
434 counts_by_source, count_work = _start_count_gather(counts, spec)
435 if permutation_output is None:
436 routed_tokens, unpermute_mapping = _permute_topk_input(hidden_states, topk_ids)
437 else:
438 routed_tokens, unpermute_mapping = _permute_topk_input_out(hidden_states, topk_ids, permutation_output)
439 received_counts = _finish_count_gather(counts_by_source, count_work, spec)
440 expert_capacity, maximum_received_slots = _expert_capacity(counts_by_source, spec)
441 return PreparedTopKRoute(
442 routed_tokens=routed_tokens,
443 unpermute_mapping=unpermute_mapping,
444 tokens_per_expert=counts,
445 received_counts=received_counts,
446 maximum_received_slots=maximum_received_slots,
447 metadata=_compute_route_metadata(
448 counts,
449 counts_by_source,
450 received_counts,
451 spec,
452 expert_capacity,
453 ),
454 )
457def _valid_route_counts(row: list[int], top_k: int) -> bool:
458 """Validate one source histogram against its actual distinct Top-K rows."""
459 return row[3] == row[1] * top_k and row[4] >= 0 and row[5] <= row[1]
462def _validate_dynamic_sizes(rows: list[list[int]], spec: MegaMoeSpec) -> None:
463 """Keep native size and legacy bound rejection ahead of route publication."""
464 flexible = spec.auto_tokens
465 if flexible and any(row[0] or row[1] < 0 for row in rows):
466 raise RuntimeError("MegaMoe dynamic route exceeds native INT32 size limits")
467 if not flexible and any(
468 row[0] or not 0 <= row[1] <= spec.max_local_num_tokens for row in rows):
469 raise RuntimeError(f"MegaMoe token capacity overflow: limit={spec.max_local_num_tokens}, "
470 f"actual={[row[1] for row in rows]}")
473def _validate_dynamic_rows(rows: list[list[int]], spec: MegaMoeSpec) -> None:
474 """Validate every rank before any automatic allocation or SHMEM write."""
475 _validate_dynamic_sizes(rows, spec)
476 if any(row[2] != rows[0][2] for row in rows):
477 raise RuntimeError("MegaMoe autograd participation differs across EP ranks")
478 if any(not _valid_route_counts(row, spec.top_k) for row in rows):
479 raise RuntimeError("MegaMoe counts must describe the actual distinct Top-K routes")
482def _dynamic_summary(payload: torch.Tensor, spec: MegaMoeSpec) -> tuple[int, int, int]:
483 """Use one Host summary for shape, participation, receive limits, and schedule."""
484 counts = payload[:, 3:]
485 loads = counts.reshape(spec.ep_size, spec.ep_size, spec.local_experts).sum(dim=(0, 2))
486 summary = torch.cat((payload[:, :3], counts.sum(1, keepdim=True),
487 counts.amin(1, keepdim=True), counts.amax(1, keepdim=True), loads[:, None]), dim=1)
488 rows = summary.tolist()
489 return _summarize_dynamic_rows(rows, spec)
492def _summarize_dynamic_rows(rows: list[list[int]], spec: MegaMoeSpec) -> tuple[int, int, int]:
493 """Keep overflow and participation checks common to both metadata policies."""
494 _validate_dynamic_rows(rows, spec)
495 maximum = max(row[6] for row in rows)
496 maximum_tokens = max(row[1] for row in rows) if spec.auto_tokens else spec.local_num_tokens
497 maximum_capacity = _align_capacity(spec.ep_size * maximum_tokens * spec.top_k)
498 if maximum > maximum_capacity:
499 raise RuntimeError(
500 f"MegaMoe receive capacity overflow: configured_capacity={maximum_capacity}, actual_maximum={maximum}"
501 )
502 plan_tokens = max(128, _align_capacity(max(row[1] for row in rows)))
503 if plan_tokens * max(spec.hidden_size, spec.top_k) > 2**31 - 1 or maximum > 2**31 - 1:
504 raise RuntimeError("MegaMoe dynamic route exceeds native INT32 size limits")
505 return max(1, rows[spec.rank_id][6]), plan_tokens, maximum
508def _read_dynamic_route(
509 payload: torch.Tensor,
510 spec: MegaMoeSpec,
511) -> tuple[tuple[int, int, int], list[list[int]] | None]:
512 """Reuse the required readback for small descriptors; reduce large tables on device."""
513 if spec.ep_size * spec.num_experts > _HOST_ROUTE_MAX_COUNTS:
514 return _dynamic_summary(payload, spec), None
515 raw = payload.tolist()
516 return _summarize_host_route(raw, spec), raw
519def _summarize_host_route(raw: list[list[int]], spec: MegaMoeSpec) -> tuple[int, int, int]:
520 """Validate a bounded Host table before allocating or publishing source rows."""
521 counts = [row[3:] for row in raw]
522 local = spec.local_experts
523 loads = [
524 sum(sum(values[start : start + local]) for values in counts) for start in range(0, spec.num_experts, local)
525 ]
526 rows = [row[:3] + [sum(values), min(values), max(values), load] for row, values, load in zip(raw, counts, loads)]
527 return _summarize_dynamic_rows(rows, spec)
530def _route_cache_signature(spec: MegaMoeSpec, device: Any) -> tuple[Any, ...]:
531 """Keep descriptor reuse specific to the route topology and size contract."""
532 return (spec.ep_size, id(spec.ep_group), spec.rank_id, spec.num_experts, spec.top_k, spec.hidden_size,
533 spec.dispatch_mode, spec.auto_tokens, spec.max_local_num_tokens, torch.device(device))
536def _read_cached_route(
537 payload: torch.Tensor, spec: MegaMoeSpec, workspace: MegaMoeWorkspace | None, device: Any,
538) -> tuple[tuple[int, int, int], list[list[int]] | None, RouteMetadata | None]:
539 """Reuse only an identical, previously validated all-rank wire table."""
540 if workspace is None or not spec.auto_tokens or spec.ep_size * spec.num_experts > _HOST_ROUTE_MAX_COUNTS:
541 summary, raw = _read_dynamic_route(payload, spec)
542 return summary, raw, None
543 cached = getattr(workspace, "route_cache", None)
544 same_topology = isinstance(cached, _HostRouteCache) and cached.signature == _route_cache_signature(spec, device)
545 remaining = getattr(workspace, "route_cache_probe_after", 0)
546 if same_topology and remaining > 0:
547 workspace.route_cache_probe_after = remaining - 1
548 return _dynamic_summary(payload, spec), None, None
549 workspace.route_cache_probe_after = 0
550 raw = payload.tolist()
551 if same_topology and cached.raw == raw:
552 cached.wait(device)
553 return cached.summary, raw, cached.metadata
554 summary = _summarize_host_route(raw, spec)
555 if same_topology and spec.ep_size * spec.num_experts > _HOST_DESCRIPTOR_MAX_COUNTS:
556 workspace.route_cache_probe_after = _ROUTE_CACHE_REPROBE_STEPS
557 return summary, raw, None
560def _cache_host_route(
561 workspace: MegaMoeWorkspace | None, spec: MegaMoeSpec, raw: list[list[int]],
562 summary: tuple[int, int, int], metadata: RouteMetadata, device: Any,
563) -> None:
564 """Retain descriptors only after confirming a repeated validated wire table."""
565 if workspace is None or not spec.auto_tokens:
566 return
567 signature = _route_cache_signature(spec, device)
568 previous = getattr(workspace, "route_cache", None)
569 repeated = isinstance(previous, _HostRouteCache) and previous.signature == signature and previous.raw == raw
570 retained = metadata if repeated else None
571 stream, ready = None, None
572 if repeated and torch.device(device).type == "npu":
573 stream = torch.npu.current_stream(device)
574 ready = torch.npu.Event()
575 ready.record(stream)
576 workspace.route_cache = _HostRouteCache(signature, raw, summary, retained, stream, ready)
579def _dynamic_route_metadata(
580 counts: torch.Tensor, gathered: torch.Tensor, host_rows: list[list[int]] | None,
581 spec: MegaMoeSpec, capacity: int,
582) -> RouteMetadata:
583 """Keep wider cache misses on device instead of rebuilding large Host descriptors."""
584 if host_rows is not None and spec.ep_size * spec.num_experts <= _HOST_DESCRIPTOR_MAX_COUNTS:
585 return _host_route_metadata(host_rows, spec, capacity, counts.device)
586 counts_by_source = gathered[:, 3:].contiguous()
587 start = spec.rank_id * spec.local_experts
588 received_counts = counts_by_source[:, start:start + spec.local_experts].contiguous()
589 # Retained dispatch sizes must not alias a caller-owned Router count buffer.
590 local_counts = counts_by_source[spec.rank_id] if spec.auto_tokens else counts
591 return _compute_route_metadata(local_counts, counts_by_source, received_counts, spec, capacity)
594def _permute_dynamic_pull(
595 hidden_states: torch.Tensor, topk_ids: torch.Tensor, spec: MegaMoeSpec, workspace: MegaMoeWorkspace | None,
596) -> tuple[torch.Tensor, torch.Tensor]:
597 """Take the source view only after automatic growth and lease acquisition."""
598 if workspace is None or not workspace.in_use or workspace.source_buffer is None:
599 raise RuntimeError("Pull permutation requires an initialized, claimed workspace.")
600 tokens = hidden_states.shape[0]
601 output = workspace.source_buffer[:tokens * spec.top_k]
602 return (_permute_topk_input_out(hidden_states, topk_ids, output) if tokens
603 else (output, topk_ids.reshape(-1)))
606def _can_stage_pull(hidden_states: torch.Tensor, spec: MegaMoeSpec) -> bool:
607 """Bound ordinary-memory permutation staging while capacity agreement is pending."""
608 return (spec.auto_tokens and spec.dispatch_mode == "pull" and hidden_states.device.type == "npu"
609 and hidden_states.numel() * spec.top_k * hidden_states.element_size() <= _PULL_STAGING_MAX_BYTES)
612def _stage_dynamic_permutation(
613 hidden_states: torch.Tensor, topk_ids: torch.Tensor, spec: MegaMoeSpec, invalid: bool,
614 workspace: MegaMoeWorkspace | None = None,
615) -> tuple[torch.Tensor, torch.Tensor] | None:
616 """Stage only valid automatic routes whose ordinary storage survives heap growth."""
617 if invalid or not spec.auto_tokens:
618 return None
619 if (spec.dispatch_mode == "push"
620 or _can_stage_pull(hidden_states, spec) and not getattr(workspace, "route_cache_probe_after", 0)):
621 return _permute_topk_input(hidden_states, topk_ids)
622 return None
625def _publish_staged_pull(
626 permuted: tuple[torch.Tensor, torch.Tensor], hidden_states: torch.Tensor,
627 spec: MegaMoeSpec, workspace: MegaMoeWorkspace | None,
628) -> tuple[torch.Tensor, torch.Tensor]:
629 """Publish staged rows only after every rank validated and committed its workspace."""
630 if workspace is None or not workspace.in_use or workspace.source_buffer is None:
631 raise RuntimeError("Pull publication requires an initialized, claimed workspace.")
632 output = workspace.source_buffer[:hidden_states.shape[0] * spec.top_k]
633 output.copy_(permuted[0])
634 return output, permuted[1]
637def _dynamic_header(
638 values: tuple[int, int, int], dtype: torch.dtype, device: Any, workspace: MegaMoeWorkspace | None,
639) -> torch.Tensor:
640 """Avoid repeated Host allocation and upload of an unchanged control header."""
641 key = (values, dtype, torch.device(device))
642 cached = getattr(workspace, "route_header_cache", None)
643 repeated = isinstance(cached, _RouteHeaderCache) and cached.key == key
644 if repeated and cached.tensor is not None:
645 cached.wait(device)
646 return cached.tensor
647 header = _upload_route_tensor(torch.tensor(values, dtype=dtype), device)
648 if workspace is not None:
649 stream, ready = None, None
650 if repeated and torch.device(device).type == "npu":
651 stream = torch.npu.current_stream(device)
652 ready = torch.npu.Event()
653 ready.record(stream)
654 workspace.route_header_cache = _RouteHeaderCache(key, header if repeated else None, stream, ready)
655 return header
658def _dynamic_route_payload(
659 hidden_states: torch.Tensor, flat_ids: torch.Tensor, spec: MegaMoeSpec,
660 supplied_counts: torch.Tensor | None, autograd_mask: int, workspace: MegaMoeWorkspace | None = None,
661) -> tuple[torch.Tensor, torch.Tensor, bool]:
662 """Pack one group-consistent wire format, including coordinated overflow flags."""
663 tokens = hidden_states.shape[0]
664 flexible = spec.auto_tokens
665 invalid = (tokens * max(spec.hidden_size, spec.top_k) > 2**31 - 1 if flexible
666 else tokens > spec.max_local_num_tokens)
667 counts = (torch.zeros(spec.num_experts, dtype=torch.int32, device=hidden_states.device) if invalid
668 else _resolve_counts(flat_ids, supplied_counts, spec))
669 payload_dtype = torch.int32 if flexible else torch.int64
670 # An invalid source must still join the same collective before all ranks reject it.
671 header_tokens = min(tokens, 2**31 - 1) if flexible else tokens
672 header = _dynamic_header((int(invalid), header_tokens, autograd_mask), payload_dtype,
673 hidden_states.device, workspace if flexible else None)
674 payload = torch.cat((header, counts.to(payload_dtype)))
675 return counts, payload, invalid
678def _start_dynamic_gather(payload: torch.Tensor, spec: MegaMoeSpec) -> tuple[torch.Tensor, Any | None]:
679 """Match receive storage to the dynamic count wire dtype."""
680 if spec.ep_size == 1:
681 gathered, work = payload.unsqueeze(0), None
682 else:
683 gathered = torch.empty((spec.ep_size, payload.numel()), dtype=payload.dtype, device=payload.device)
684 work = dist.all_gather_into_tensor(gathered.reshape(-1), payload, group=spec.ep_group, async_op=True)
685 return gathered, work
688def _prepare_dynamic_route(
689 hidden_states: torch.Tensor, topk_ids: torch.Tensor, flat_ids: torch.Tensor, spec: MegaMoeSpec,
690 supplied_counts: torch.Tensor | None, workspace: MegaMoeWorkspace | None, autograd_mask: int,
691 prepare_workspace: Callable[[int, int], None] | None = None,
692) -> PreparedTopKRoute:
693 """Exchange variable-length metadata without adding a token-count collective."""
694 tokens = hidden_states.shape[0]
695 counts, payload, invalid = _dynamic_route_payload(
696 hidden_states, flat_ids, spec, supplied_counts, autograd_mask, workspace)
697 if workspace is not None and spec.dispatch_mode != "pull":
698 workspace.wait_for_reuse()
699 gathered, work = _start_dynamic_gather(payload, spec)
700 # Ordinary storage remains valid if count agreement triggers a SHMEM restart.
701 permuted = _stage_dynamic_permutation(hidden_states, topk_ids, spec, invalid, workspace)
702 summary, host_rows, metadata = None, None, None
703 if spec.auto_tokens:
704 if work is not None:
705 work.wait()
706 work = None
707 summary, host_rows, metadata = _read_cached_route(gathered, spec, workspace, hidden_states.device)
708 if prepare_workspace is None:
709 raise RuntimeError("Automatic MegaMoe routes require workspace capacity preparation")
710 prepare_workspace(summary[1], summary[2])
711 if invalid:
712 permuted = None
713 elif spec.dispatch_mode == "pull":
714 permuted = (_permute_dynamic_pull(hidden_states, topk_ids, spec, workspace) if permuted is None else
715 _publish_staged_pull(permuted, hidden_states, spec, workspace))
716 elif permuted is None:
717 permuted = _permute_topk_input(hidden_states, topk_ids)
718 if work is not None:
719 work.wait()
720 if summary is None:
721 summary, host_rows = _read_dynamic_route(gathered, spec)
722 capacity, plan_tokens, maximum = summary
723 if metadata is None:
724 metadata = _dynamic_route_metadata(counts, gathered, host_rows, spec, capacity)
725 if host_rows is not None:
726 _cache_host_route(workspace, spec, host_rows, summary, metadata, hidden_states.device)
727 received_counts = metadata.combine_size.reshape(spec.ep_size, spec.local_experts)
728 return PreparedTopKRoute(
729 routed_tokens=permuted[0], unpermute_mapping=permuted[1], tokens_per_expert=counts,
730 received_counts=received_counts,
731 maximum_received_slots=maximum,
732 metadata=replace(metadata, local_num_tokens=tokens, plan_tokens=plan_tokens),
733 )
736def restore_topk_output(
737 expert_output: torch.Tensor,
738 unpermute_mapping: torch.Tensor,
739 topk_weights: torch.Tensor,
740) -> torch.Tensor:
741 """Restore token order and apply Router weights with the native NPU op.
743 Args:
744 expert_output: Expert outputs in the expanded routing order.
745 unpermute_mapping: Inverse mapping retained by the source permutation.
746 topk_weights: Router weights for the original token rows.
748 Returns:
749 Weighted expert outputs in the original token order.
750 """
751 if topk_weights.shape[0] == 0:
752 # Keep both the expert backward and the empty routing-weight gradient connected.
753 return expert_output[:0] + topk_weights.sum().to(expert_output.dtype)
754 return load_torch_npu().npu_moe_token_unpermute(
755 expert_output,
756 unpermute_mapping,
757 probs=topk_weights.float().contiguous(),
758 )