Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed / expert_parallel / ep_context.py: 89%
76 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Routed-slot transport, independent of routing and expert computation."""
17from __future__ import annotations
19from dataclasses import dataclass
20from typing import Any
22import torch
23from torch import Tensor
25from hyper_parallel.distributed.expert_parallel.collectives import (
26 ep_all_to_all, ep_exchange_counts, ep_split_sizes,
27)
30@dataclass
31class DispatchReceipt:
32 """Per-call inverse permutations and split sizes; never cached on a module."""
34 owner: object
35 send_counts: list[int]
36 recv_counts: list[int]
37 send_order: Tensor
38 expert_order: Tensor
39 tokens: int
40 top_k: int
41 consumed: bool = False
44@dataclass(frozen=True)
45class DispatchedExperts:
46 """Expert-major slots; probability application belongs to the expert algorithm."""
48 hidden_states: Tensor
49 routing_weights: Tensor
50 expert_indices: Tensor
51 tokens_per_expert: Tensor
52 receipt: DispatchReceipt
55def _validate_slots(hidden: Tensor, indices: Tensor, weights: Tensor, experts: int) -> None:
56 """Validate routed token slots, expert indices and optional routing probabilities."""
57 if hidden.ndim != 2 or indices.ndim != 2 or weights.shape != indices.shape:
58 raise ValueError("expected hidden[T,H], expert_indices[T,K], routing_weights[T,K]")
59 if hidden.shape[0] != indices.shape[0] or indices.shape[1] < 1:
60 raise ValueError("token dimensions must agree and top-k must be positive")
61 if indices.dtype not in (torch.int32, torch.int64) or not weights.is_floating_point():
62 raise ValueError("expert IDs must be integers and routing weights floating point")
63 if hidden.device != indices.device or hidden.device != weights.device:
64 raise ValueError("all routing inputs must be on the same device")
65 if bool(((indices < 0) | (indices >= experts)).any()):
66 raise ValueError("every supplied slot must have an expert ID in [0, num_experts); drop is unsupported")
69class EPContext:
70 """Balanced contiguous expert ownership from HP mesh and Shard(0) layout."""
72 def __init__(self, mesh: Any, *, num_experts: int) -> None:
73 """Borrow the EP mesh; never create a group or reduce parameter gradients."""
74 if mesh is None or mesh.ndim != 1:
75 raise ValueError("EPContext requires an existing one-dimensional HP mesh")
76 if num_experts < 1 or num_experts % mesh.size():
77 raise ValueError("num_experts must be positive and divisible by EP size")
78 self.mesh = mesh
79 self.num_experts = num_experts
80 self.local_experts = num_experts // mesh.size()
81 self.rank = mesh.get_local_rank()
82 self._identity = object()
84 def _exchange(self, tensor, send, receive):
85 if self.mesh.size() == 1:
86 return tensor
87 return ep_all_to_all(tensor, send, receive, self.mesh.get_group())
89 def dispatch(self, hidden_states: Tensor, expert_indices: Tensor, routing_weights: Tensor) -> DispatchedExperts:
90 """Transport every already-selected slot, including zero-probability slots.
92 Args:
93 hidden_states: Source tokens [T,H].
94 expert_indices: Global selected expert IDs [T,K]; no capacity/drop mode.
95 routing_weights: Already normalized/scaled probabilities [T,K], with gradients.
96 """
97 _validate_slots(hidden_states, expert_indices, routing_weights, self.num_experts)
98 tokens, top_k = expert_indices.shape
99 indices = expert_indices.reshape(-1).to(torch.int64)
100 destinations = indices // self.local_experts
101 send_order = torch.argsort(destinations, stable=True)
102 counts = torch.bincount(destinations, minlength=self.mesh.size())
103 received_counts = (ep_exchange_counts(counts, self.mesh.get_group())
104 if self.mesh.size() > 1 else counts)
105 send, receive = ep_split_sizes(counts), ep_split_sizes(received_counts)
106 expanded = hidden_states[:, None, :].expand(-1, top_k, -1).reshape(-1, hidden_states.shape[-1])
107 values = self._exchange(expanded[send_order], send, receive)
108 weights = self._exchange(routing_weights.reshape(-1, 1)[send_order], send, receive).reshape(-1)
109 ids = self._exchange(indices[send_order, None], send, receive).reshape(-1) - self.rank * self.local_experts
110 expert_order = torch.argsort(ids, stable=True)
111 receipt = DispatchReceipt(self._identity, send, receive, send_order, expert_order, tokens, top_k)
112 return DispatchedExperts(values[expert_order], weights[expert_order], ids[expert_order],
113 torch.bincount(ids, minlength=self.local_experts), receipt)
115 def combine(self, values: Tensor, receipt: DispatchReceipt) -> Tensor:
116 """Return computed slots to their source tokens and sum top-k contributions.
118 Args:
119 values: Expert-major weighted slot outputs [received_slots,H].
120 receipt: Receipt from exactly one dispatch on this context.
121 """
122 if receipt.owner is not self._identity or receipt.consumed:
123 raise ValueError("dispatch receipt belongs to another context or was already consumed")
124 if values.ndim != 2 or values.shape[0] != sum(receipt.recv_counts):
125 raise ValueError("expert output must have one row per received slot")
126 peer_major = values[torch.argsort(receipt.expert_order)]
127 source_slots = self._exchange(peer_major, receipt.recv_counts, receipt.send_counts)
128 source_slots = source_slots[torch.argsort(receipt.send_order)]
129 output = source_slots.reshape(receipt.tokens, receipt.top_k, values.shape[-1]).sum(dim=1)
130 receipt.consumed = True
131 return output