Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_multicore / scheduler / graph.py: 97%
101 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"""
16Compute graph abstractions: OpType, TensorSpec, SplitSpec, OperatorNode, ComputeGraph.
18ComputeGraph.propagate_splits() traverses the DAG in topological order and
19automatically computes task_num / split_dim for every node from their SplitSpec.
20"""
21from collections import deque
22from dataclasses import dataclass, field
23from enum import Enum
24from typing import Any, Callable, List, Optional, Tuple, Union
26from hyper_multicore.scheduler.config import TaskSplitValue
29class OpType(Enum):
30 """Operator kinds supported by the multicore compute graph."""
32 ALLTOALL = "alltoall"
33 AIV_PIPELINE = "aiv_pipeline"
34 GMM = "gmm"
35 SWIGLU = "swiglu"
36 SWIGLU_GRAD = "swiglu_grad"
37 MHC_POST = "mhc_post"
38 MHC_NORM_CAST = "mhc_norm_cast"
39 MHC_PROJECTION = "mhc_projection"
40 MHC_MAPPING = "mhc_mapping"
41 MHC_INPUT_MIX = "mhc_input_mix"
42 MHC_PIPELINE = "mhc_pipeline"
43 RMS_NORM = "rms_norm"
46@dataclass
47class TensorSpec:
48 """Describes a single tensor in the graph (a graph edge / shared buffer)."""
49 name: str
50 shape: list
51 param_position: int
52 dtype_size: int = 2 # bf16=2, int64=8, int32=4
53 tensor_type: int = 0 # 0=tensor, 1=tensorlist
54 transpose: bool = False
55 is_dynamic: bool = False
57 # Set by propagate_splits(); not user-supplied.
58 split_dim: int = field(default=-1, init=False)
59 split_num: int = field(default=1, init=False)
62@dataclass
63class SplitSpec:
64 """
65 Declarative split specification attached to an OperatorNode.
67 split_inputs
68 None → source operator; always computes via task_num_fn.
69 list → list of (input_idx, dim) pairs; ALL must already be split on
70 the given dim for this op to be active.
72 split_output_dims
73 Per-output split axis. -1 = leave un-split.
75 task_num_fn
76 Callable(tsv) -> int. Returns task_num when the split condition holds.
77 """
78 split_inputs: Optional[List[Tuple[int, int]]]
79 task_num_fn: Callable
80 split_output_dims: List[int] = field(default_factory=lambda: [0])
83@dataclass
84class OperatorNode:
85 """A single operator node in the compute DAG."""
86 name: str
87 op_type: OpType
88 inputs: List[TensorSpec]
89 outputs: List[TensorSpec]
90 param_positions: List[int]
91 split_value: int
92 split_spec: SplitSpec
93 tiling_position: int
94 fill_config: Any # FillConfig subclass instance
95 kernel_spec: Any = None # reserved for @MultiCore decorator path
96 diagnostic_name: Optional[str] = None # optional Host-side diagnostic display name
97 diagnostic_view_resolver: Optional[Callable[..., Any]] = None
98 diagnostic_host_ports: bool = False # Native hard-coded ABI operands, not TaskDesc TensorDesc arrays.
100 predecessors: List['OperatorNode'] = field(default_factory=list)
101 successors: List['OperatorNode'] = field(default_factory=list)
102 task_num: int = field(default=0, init=False)
105class ComputeGraph:
106 """Directed acyclic graph describing operator execution order."""
108 def __init__(self) -> None:
109 """Initialize an empty compute graph."""
110 self._nodes: dict[str, OperatorNode] = {}
111 self._insertion_order: list[str] = []
113 def add_op(self, op: OperatorNode) -> 'ComputeGraph':
114 """Add an operator node to the graph.
116 Args:
117 op: Operator node to append.
118 """
119 self._nodes[op.name] = op
120 self._insertion_order.append(op.name)
121 return self
123 def add_edge(
124 self, src: Union[OperatorNode, str], dst: Union[OperatorNode, str]
125 ) -> 'ComputeGraph':
126 """Connect two operator nodes.
128 Args:
129 src: Source node or its registered name.
130 dst: Destination node or its registered name.
131 """
132 s = src if isinstance(src, OperatorNode) else self._nodes[src]
133 d = dst if isinstance(dst, OperatorNode) else self._nodes[dst]
134 s.successors.append(d)
135 d.predecessors.append(s)
136 return self
138 def get_op(self, name: str) -> OperatorNode:
139 """Retrieve an operator node by name.
141 Args:
142 name: Registered operator name.
143 """
144 return self._nodes[name]
146 def topological_sort(self) -> List[OperatorNode]:
147 """Kahn's algorithm; insertion order used for tie-breaking."""
148 in_deg = {n: len(op.predecessors) for n, op in self._nodes.items()}
149 queue = deque(n for n in self._insertion_order if in_deg[n] == 0)
150 order = []
151 while queue:
152 n = queue.popleft()
153 order.append(self._nodes[n])
154 for succ in self._nodes[n].successors:
155 in_deg[succ.name] -= 1
156 if in_deg[succ.name] == 0:
157 queue.append(succ.name)
158 if len(order) != len(self._nodes):
159 raise ValueError("ComputeGraph has a cycle")
160 return order
162 def propagate_splits(self, tsv: TaskSplitValue) -> None:
163 """
164 Compute task_num and split_dim for every node from their SplitSpec.
166 Must be called once after graph construction and before any fill loop.
167 Resets all TensorSpec split annotations before traversal.
169 Args:
170 tsv: Topology values used to resolve split counts.
171 """
172 for op in self._nodes.values():
173 for t in op.inputs + op.outputs:
174 t.split_dim = -1
175 t.split_num = 1
177 for op in self.topological_sort():
178 ss = op.split_spec
179 if ss.split_inputs is None:
180 task_num = ss.task_num_fn(tsv)
181 else:
182 if all(op.inputs[idx].split_dim == dim for idx, dim in ss.split_inputs):
183 task_num = ss.task_num_fn(tsv)
184 else:
185 task_num = 1
187 op.task_num = task_num
189 for i, out in enumerate(op.outputs):
190 if i < len(ss.split_output_dims):
191 d = ss.split_output_dims[i]
192 out.split_dim = d if (task_num > 1 and d >= 0) else -1
193 else:
194 out.split_dim = -1
195 out.split_num = task_num