Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_multicore / diagnostics / common / graph.py: 90%
148 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"""Business-independent graph contracts and diagnostic task identity."""
17from __future__ import annotations
19from collections.abc import Callable, Mapping
20from copy import deepcopy
21from dataclasses import dataclass
22from types import MappingProxyType
23from typing import Any
25from hyper_multicore.scheduler.config import RuntimeConfigC, TaskDescC, TaskType
26from hyper_multicore.scheduler.graph import ComputeGraph, OperatorNode
29@dataclass(frozen=True)
30class DiagnosticSpec:
31 """Stable kernel identity and optional business-owned task-owner resolver."""
33 kernel_name: str
34 owner_label: str = "Owner"
35 owner_resolver: Callable[[OperatorNode, TaskDescC, Any], int | None] | None = None
36 owner_context: Any = None
37 execution_mode: str = "event_driven"
38 vector_worker_stride: int = 2
39 graph_adapter: Callable[[Any, RuntimeConfigC], ComputeGraph] | None = None
42@dataclass(frozen=True)
43class GraphDiagnosticMetadata:
44 """Host-only task identity, independent of trace or Tensor-dump formats."""
46 kernel_name: str
47 owner_label: str
48 stage_names: Mapping[int, str]
49 task_stage_ids: Mapping[int, int]
50 task_stage_names: Mapping[int, str]
51 task_owner_ids: Mapping[int, int | None]
54def bind_graph_tensor_ports(
55 graph: ComputeGraph,
56 ports: Mapping[str, tuple[list[Any], list[Any]]], resolver: Callable[..., Any],
57) -> ComputeGraph:
58 """Bind real GM operands on a diagnostic-only copy of a scheduling graph.
60 Native task builders may describe dependencies without encoding tensor
61 operands. The business adapter supplies the actual input-list positions;
62 neither the scheduling graph nor the normal Device runtime is modified.
64 Args:
65 graph: Business compute graph whose scheduling storage remains unchanged.
66 ports: Mapping from operator names to actual native input and output operands.
67 resolver: Business callback that resolves a selected operand's contiguous range.
68 """
69 graph = deepcopy(graph)
70 for operator in graph.topological_sort():
71 operator.inputs, operator.outputs = ports[operator.name]
72 operator.param_positions = [tensor.param_position for tensor in operator.inputs + operator.outputs]
73 operator.diagnostic_view_resolver = resolver
74 operator.diagnostic_host_ports = True
75 return graph
78def operator_diagnostic_name(operator: OperatorNode) -> str:
79 """Return an explicit diagnostic name or derive one from the graph node name.
81 Args:
82 operator: Graph operator associated with the selected task.
83 """
84 diagnostic_name = operator.diagnostic_name
85 if diagnostic_name is not None:
86 if not isinstance(diagnostic_name, str) or not diagnostic_name.strip():
87 raise ValueError(
88 f"OperatorNode.diagnostic_name must be a non-empty string when set: {diagnostic_name!r}"
89 )
90 return diagnostic_name.strip()
91 if not isinstance(operator.name, str) or not operator.name.strip():
92 raise ValueError(f"OperatorNode.name must be a non-empty string, got {operator.name!r}")
93 stage_name = "".join(part[:1].upper() + part[1:] for part in operator.name.strip().split("_") if part)
94 if not stage_name:
95 raise ValueError(f"OperatorNode.name must contain a diagnostic display character: {operator.name!r}")
96 return stage_name
99def _scheduled_task_ids(runtime_config: RuntimeConfigC) -> set[int]:
100 """Collect queue task IDs, including tasks outside RuntimeConfig.task_num."""
101 task_ids = set()
102 task_schedules = (
103 (runtime_config.cube_task_indices, int(runtime_config.task_index_num[0])),
104 (runtime_config.vector_task_indices, int(runtime_config.task_index_num[1])),
105 (runtime_config.mix_task_indices, int(runtime_config.task_index_num[2])),
106 )
107 for task_indices, scheduled_task_count in task_schedules:
108 if not 0 <= scheduled_task_count <= len(task_indices):
109 raise ValueError(f"scheduled task count is outside RuntimeConfig capacity: {scheduled_task_count}")
110 for schedule_index in range(scheduled_task_count):
111 task_id = int(task_indices[schedule_index])
112 if not 0 <= task_id < len(runtime_config.all_tasks):
113 raise ValueError(f"scheduled task ID is outside RuntimeConfig capacity: {task_id}")
114 task_ids.add(task_id)
115 return task_ids
118def _validate_parameter_position(position: int, parameter_count: int, label: str) -> None:
119 if isinstance(position, bool) or not isinstance(position, int) or not 0 <= position < parameter_count:
120 raise ValueError(f"{label} parameter position {position!r} is outside ABI parameter_count={parameter_count}")
123def _validate_operator(operator: OperatorNode, parameter_count: int) -> None:
124 """Validate graph operand names, shapes and native ABI bindings."""
125 if isinstance(operator.task_num, bool) or not isinstance(operator.task_num, int) or operator.task_num < 0:
126 raise ValueError(f"operator {operator.name!r} has invalid task_num={operator.task_num!r}")
127 ports = operator.inputs + operator.outputs
128 if len(operator.param_positions) != len(ports):
129 raise ValueError(f"operator {operator.name!r} param_positions must match its input/output ports")
130 for position, tensor_spec in zip(operator.param_positions, ports):
131 _validate_parameter_position(tensor_spec.param_position, parameter_count, tensor_spec.name)
132 if position != tensor_spec.param_position:
133 raise ValueError(f"operator {operator.name!r} param_positions disagrees with Tensor {tensor_spec.name!r}")
134 if not isinstance(tensor_spec.name, str) or not tensor_spec.name.strip():
135 raise ValueError("TensorSpec.name must be a non-empty string")
136 if any(isinstance(dim, bool) or not isinstance(dim, int) or dim < 0 for dim in tensor_spec.shape):
137 raise ValueError(f"Tensor {tensor_spec.name!r} shape must contain non-negative integers")
138 if (isinstance(tensor_spec.dtype_size, bool) or not isinstance(tensor_spec.dtype_size, int)
139 or tensor_spec.dtype_size <= 0):
140 raise ValueError(f"Tensor {tensor_spec.name!r} dtype_size must be a positive integer")
143def _validate_task_ports(
144 task: TaskDescC, operator: OperatorNode, task_id: int,
145) -> None:
146 """Check a scheduled task's operand descriptors against the graph contract."""
147 if operator.diagnostic_host_ports:
148 if not callable(operator.diagnostic_view_resolver):
149 raise ValueError(f"native ABI stage {operator.name} requires a view resolver")
150 return
151 for io, descriptors, specs, count in (
152 ("input", task.inputs, operator.inputs, int(task.num_inputs)),
153 ("output", task.outputs, operator.outputs, int(task.num_outputs)),
154 ):
155 if count != len(specs) or count > len(descriptors):
156 raise ValueError(f"task {task_id} stage {operator.name!r} {io} count does not match graph/ABI capacity")
157 for index, spec in enumerate(specs):
158 descriptor = descriptors[index]
159 if int(descriptor.input_position) != spec.param_position:
160 raise ValueError(f"task {task_id} {io}{index} parameter position disagrees with Tensor {spec.name!r}")
161 if int(descriptor.data_type) != spec.dtype_size:
162 raise ValueError(f"task {task_id} {io}{index} element byte size disagrees with Tensor {spec.name!r}")
165def _validate_task_schedule(runtime_config: RuntimeConfigC, graph_task_count: int) -> None:
166 """Reject missing, duplicated or unmapped compute tasks in Device queues."""
167 queues = (runtime_config.cube_task_indices, runtime_config.vector_task_indices, runtime_config.mix_task_indices)
168 scheduled_ids = _scheduled_task_ids(runtime_config)
169 seen = set()
170 for indices, count in zip(queues, runtime_config.task_index_num[:3]):
171 for task_id in indices[:count]:
172 if task_id in seen:
173 raise ValueError(f"task {task_id} appears more than once in Device schedules")
174 seen.add(task_id)
175 missing = set(range(graph_task_count)) - scheduled_ids
176 if missing:
177 raise ValueError(f"graph tasks are absent from Device schedules: {sorted(missing)}")
178 for task_id in scheduled_ids | set(range(int(runtime_config.task_num))):
179 if task_id >= graph_task_count and runtime_config.all_tasks[task_id].task_type != TaskType.TASK_TERMINATE:
180 raise ValueError(f"task {task_id} has no graph stage and is not a termination task")
183def validate_mega_kernel_diagnostic_contract(
184 graph: ComputeGraph,
185 runtime_config: RuntimeConfigC,
186 parameter_count: int,
187) -> None:
188 """Validate graph/task/ABI agreement before allocating Device diagnostic resources.
190 Args:
191 graph: Propagated graph; task IDs must follow contiguous topological ranges.
192 runtime_config: Filled graph-sized Host configuration including Device queues.
193 parameter_count: Number of launch binding entries. Device task ports
194 must use real native input_list positions. Zero-task nodes may
195 describe Host-visible results produced by an enclosing native call.
197 Raises:
198 TypeError: Invalid graph or runtime configuration.
199 ValueError: Inconsistent graph/task/ABI mapping.
201 Note:
202 This read-only check does not call view resolvers or inspect Device Tensors.
203 Actual offset/shape memory bounds are validated when L1 prepares a launch.
204 """
205 if not isinstance(graph, ComputeGraph):
206 raise TypeError("graph must be ComputeGraph")
207 if not isinstance(runtime_config, RuntimeConfigC) or not hasattr(runtime_config, "all_tasks"):
208 raise TypeError("runtime_config must be an allocated graph-sized RuntimeConfigC")
209 if isinstance(parameter_count, bool) or not isinstance(parameter_count, int) or parameter_count < 0:
210 raise ValueError("parameter_count must be a non-negative integer")
211 if runtime_config.task_num > len(runtime_config.all_tasks):
212 raise ValueError("RuntimeConfig.task_num exceeds allocated task capacity")
213 task_id = 0
214 for operator in graph.topological_sort():
215 _validate_operator(operator, parameter_count)
216 if task_id + operator.task_num > runtime_config.task_num:
217 raise ValueError(f"graph tasks for stage {operator.name!r} exceed RuntimeConfig.task_num")
218 for current_task_id in range(task_id, task_id + operator.task_num):
219 _validate_task_ports(runtime_config.all_tasks[current_task_id], operator, current_task_id)
220 task_id += operator.task_num
221 _validate_task_schedule(runtime_config, task_id)
224def build_graph_metadata(
225 graph: ComputeGraph, runtime_config: RuntimeConfigC, spec: DiagnosticSpec,
226) -> GraphDiagnosticMetadata:
227 """Resolve stable graph stage IDs and optional owners without modifying tasks.
229 Args:
230 graph: Business compute graph whose scheduling storage remains unchanged.
231 runtime_config: Complete Host configuration describing native task queues.
232 spec: Tool-neutral kernel identity and optional business owner callbacks.
233 """
234 for label, value in (("kernel_name", spec.kernel_name), ("owner_label", spec.owner_label)):
235 if not isinstance(value, str) or not value.strip():
236 raise ValueError(f"{label} must be a non-empty string, got {value!r}")
237 if spec.owner_resolver is not None and not callable(spec.owner_resolver):
238 raise TypeError("owner_resolver must be callable or None")
239 stages = {}
240 task_stages = {}
241 task_names = {}
242 owners = {}
243 task_id = 0
244 for stage_id, operator in enumerate(graph.topological_sort()):
245 name = operator_diagnostic_name(operator)
246 stages[stage_id] = name
247 for current_id in range(task_id, task_id + operator.task_num):
248 owner = None
249 if spec.owner_resolver is not None:
250 owner = spec.owner_resolver(operator, runtime_config.all_tasks[current_id], spec.owner_context)
251 if owner is not None and (isinstance(owner, bool) or not isinstance(owner, int)
252 or not 0 <= owner < 0xFFFFFFFF):
253 raise ValueError(f"diagnostic owner_id must be a uint32 integer or None, got {owner!r}")
254 task_stages[current_id] = stage_id
255 task_names[current_id] = name
256 owners[current_id] = owner
257 task_id += operator.task_num
258 return GraphDiagnosticMetadata(
259 spec.kernel_name.strip(), spec.owner_label.strip(),
260 MappingProxyType(stages), MappingProxyType(task_stages),
261 MappingProxyType(task_names), MappingProxyType(owners),
262 )