Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_multicore / _loader.py: 71%
164 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"""Locate and load the component-owned multicore payload."""
17from __future__ import annotations
19import ctypes
20import json
21from functools import lru_cache
22import importlib
23import os
24from pathlib import Path
25import platform
26import sys
27from types import ModuleType
28from contextlib import contextmanager
29from threading import RLock
31from hyper_multicore._native_manifest import (
32 framework_package, check_integrity, read_manifest, resource_path, select_variant, select_shmem_backend,
33)
35_VENDOR_NAME = "hyper_multicore_nn"
36_NATIVE_LOCK = RLock()
37_selection = None
38_native_failure = None
39_loaded_kinds = set()
40_ACL_DEV_ATTR_NPU_ARCH = 601
43class NativeComponentUnavailableError(RuntimeError):
44 """Report an unavailable or incorrectly activated multicore component."""
47def diagnostic_context() -> str:
48 """Read best-effort environment/build metadata without importing either framework."""
49 actual = {"python": sys.executable, "python_abi": f"cp{sys.version_info.major}{sys.version_info.minor}",
50 "host_arch": platform.machine(), "cann_version": "unknown"}
51 for package in ("torch", "torch-npu"):
52 try:
53 actual[package] = framework_package(package.replace("-", "_"))["version"]
54 # Diagnostics must not replace the original import/load failure.
55 except Exception: # pylint: disable=broad-exception-caught
56 actual[package] = "unavailable"
57 try:
58 for line in (Path(os.environ["ASCEND_HOME_PATH"]) / "opp/version.info").read_text().splitlines():
59 key, separator, value = line.partition("=")
60 if separator and key.strip() == "Version":
61 actual["cann_version"] = value.strip()
62 break
63 except Exception: # pylint: disable=broad-exception-caught
64 pass
65 try:
66 info = json.loads((_component_root().parent / "build_info.json").read_text())
67 matrix = [{key: variant.get(key) for key in ("id", "torch_version", "pta_version", "torch_cxx11_abi")}
68 for variant in info.get("variants", [])]
69 except Exception: # pylint: disable=broad-exception-caught
70 matrix = "unavailable"
71 return f"runtime={actual} packaged_matrix={matrix}"
74@lru_cache(maxsize=1)
75def load_torch_npu() -> ModuleType:
76 """Register the optional NPU backend only when an operation needs it.
78 Import errors propagate so an unusable backend cannot masquerade as successful
79 native initialization. Failed imports are not cached and may be retried.
80 """
81 try:
82 framework_package("torch_npu")
83 except ModuleNotFoundError as error:
84 raise NativeComponentUnavailableError(
85 f"[HYPER-MULTICORE-PTA-NOT-INSTALLED] Python={sys.executable}; install torch-npu "
86 "for your Torch/CANN environment. Inspect hyper_multicore_build_info for packaged branches. "
87 f"{diagnostic_context()}"
88 ) from error
89 try:
90 return importlib.import_module("torch_npu")
91 except (ImportError, OSError, RuntimeError) as error:
92 raise NativeComponentUnavailableError(
93 f"[HYPER-MULTICORE-PTA-IMPORT-FAILED] {diagnostic_context()}: {error}"
94 ) from error
97def require_multicore_environment() -> Path:
98 """Require the packaged custom OPP environment prepared by ``set_env.bash``."""
99 component_root = _component_root()
100 vendor_root = component_root / "vendors" / _VENDOR_NAME
101 if not vendor_root.is_dir():
102 raise NativeComponentUnavailableError(
103 f"[HP-NATIVE-PAYLOAD-MISSING] component=multicore vendor={vendor_root}. "
104 "Inspect the current build log; for source/PYTHONPATH development, run "
105 "./build.sh."
106 )
107 vendor_root = vendor_root.resolve()
108 op_api_root = (vendor_root / "op_api" / "lib").resolve()
109 missing_variables = []
110 if not _environment_contains_path("ASCEND_CUSTOM_OPP_PATH", vendor_root):
111 missing_variables.append("ASCEND_CUSTOM_OPP_PATH")
112 if not _environment_contains_path("LD_LIBRARY_PATH", op_api_root):
113 missing_variables.append("LD_LIBRARY_PATH")
114 if missing_variables:
115 set_env_script = component_root / "set_env.bash"
116 missing_text = ",".join(missing_variables)
117 if any(module in sys.modules for module in ("torch", "torch_npu")):
118 raise NativeComponentUnavailableError(
119 "[HP-NATIVE-OPP-ACTIVATION-TOO-LATE] component=multicore "
120 f"missing={missing_text}. Torch/torch_npu has already been imported; "
121 f"exit the current Python process, run source {set_env_script}, and start a new process."
122 )
123 raise NativeComponentUnavailableError(
124 "[HP-NATIVE-OPP-NOT-ACTIVATED] component=multicore "
125 f"missing={missing_text}. Run source {set_env_script} before starting Python with multicore."
126 )
127 return vendor_root.resolve()
130def get_multicore_paths() -> tuple[Path, Path]:
131 """Return the vendor root and shared Torch adapter after ABI validation."""
132 vendor_root = require_multicore_environment()
133 selected = validate_native_compatibility()
134 return vendor_root, resource_path(selected["root"], selected["variant"]["torch_so"])
137def _runtime_identity() -> dict:
138 """Read branch and ABI selectors without comparing external binary fingerprints."""
139 try:
140 pta = framework_package("torch_npu")["version"]
141 except ModuleNotFoundError as error:
142 raise NativeComponentUnavailableError(
143 f"[HYPER-MULTICORE-PTA-NOT-INSTALLED] Python={sys.executable}; "
144 "install matching Torch/PTA/CANN; inspect hyper_multicore_build_info."
145 ) from error
146 torch = importlib.import_module("torch")
148 actual = {"torch_version": str(getattr(torch, "__version__", "unknown")), "pta_version": pta,
149 "python_abi": f"cp{sys.version_info.major}{sys.version_info.minor}",
150 "host_arch": platform.machine(), "torch_cxx11_abi": torch.compiled_with_cxx11_abi(),
151 "cann_version": "unknown"}
152 try:
153 version_file = Path(os.environ["ASCEND_HOME_PATH"]) / "opp/version.info"
154 for line in version_file.read_text().splitlines():
155 name, separator, value = line.partition("=")
156 if separator and name.strip() == "Version":
157 actual["cann_version"] = value.strip()
158 break
159 except (OSError, KeyError):
160 pass
161 return actual
164def validate_native_compatibility() -> dict:
165 """Choose an internally validated framework variant; external post/CANN values do not gate it."""
166 global _selection
167 actual = _runtime_identity() # Import the framework outside our native-registration lock.
168 with _NATIVE_LOCK:
169 if _selection is not None:
170 return _selection
171 root = _component_root().parent
172 try:
173 info = json.loads((root / "build_info.json").read_text(encoding="utf-8"))
174 manifest = read_manifest(root)
175 check_integrity(root, info, manifest)
176 variant = select_variant(manifest, actual)
177 except (OSError, ValueError, KeyError, RuntimeError) as error:
178 raise NativeComponentUnavailableError(
179 f"[HYPER-MULTICORE-ABI-MISMATCH] root={root} runtime={actual}: {error}"
180 ) from error
181 _selection = {"root": root, "variant": variant, "actual": actual, "build_info": info, "manifest": manifest}
182 return _selection
185def query_npu_arch() -> int:
186 """Read visible logical device 0's architecture without creating a device context."""
187 acl = ctypes.CDLL("libascendcl.so", mode=ctypes.RTLD_LOCAL)
188 query = acl.aclrtGetDeviceInfo
189 query.argtypes = [ctypes.c_uint32, ctypes.c_int32, ctypes.POINTER(ctypes.c_int64)]
190 query.restype = ctypes.c_int32
191 arch = ctypes.c_int64()
192 status = query(0, _ACL_DEV_ATTR_NPU_ARCH, ctypes.byref(arch))
193 if status != 0:
194 raise NativeComponentUnavailableError(f"aclrtGetDeviceInfo(NPU_ARCH) failed for visible device 0: {status}")
195 return arch.value
198def select_native_shmem(selected: dict) -> tuple[str, dict]:
199 """Select a packaged backend before any project SHMEM library is loaded."""
200 try:
201 return select_shmem_backend(selected["manifest"], query_npu_arch())
202 except (OSError, AttributeError, ValueError) as error:
203 raise NativeComponentUnavailableError(f"[HP-NATIVE-SHMEM-BACKEND-UNAVAILABLE] {error}") from error
206def native_ready(kind: str) -> bool:
207 """Check terminal state even when a previously loaded adapter takes its fast path."""
208 with _NATIVE_LOCK:
209 if _native_failure is not None:
210 raise NativeComponentUnavailableError(
211 f"[HYPER-MULTICORE-INITIALIZATION-FAILED] Restart the process: {_native_failure}"
212 ) from _native_failure
213 return kind in _loaded_kinds
216@contextmanager
217def native_loading(kind: str):
218 """Serialize initial registration and make partial native initialization failures terminal."""
219 global _native_failure
220 with _NATIVE_LOCK:
221 if _native_failure is not None:
222 raise NativeComponentUnavailableError(
223 f"[HYPER-MULTICORE-INITIALIZATION-FAILED] Restart the process: {_native_failure}"
224 ) from _native_failure
225 if kind in _loaded_kinds:
226 yield False
227 return
228 try:
229 yield True
230 except BaseException as error:
231 _native_failure = error
232 raise
233 _loaded_kinds.add(kind)
236def native_context() -> str:
237 """Describe the fixed adapter and runtime without hiding the original loader failure."""
238 if _selection is None:
239 return diagnostic_context()
240 record = next(v for v in _selection["build_info"]["variants"] if v["id"] == _selection["variant"]["id"])
241 return f"runtime={_selection['actual']} adapter={_selection['variant']} built={record}"
244def preload_vendor_library(vendor_root: Path) -> None:
245 """Load the exact ACLNN library without exposing its dependency symbols globally."""
246 library_path = vendor_root / "op_api" / "lib" / "libcust_opapi.so"
247 try:
248 # CANN libraries can export identically named globals. Global promotion
249 # can bind their independent destructors to the same object at exit.
250 # ACLNN entry points are resolved through the vendor library handle.
251 ctypes.CDLL(str(library_path), mode=ctypes.RTLD_LOCAL)
252 except OSError as error:
253 raise NativeComponentUnavailableError(
254 f"[HP-NATIVE-VENDOR-LOAD-FAILED] library={library_path} {native_context()} error={error}."
255 ) from error
258def _component_root() -> Path:
259 """Return the installed or source-build multicore payload root."""
260 packaged = Path(__file__).resolve().parent / "lib"
261 if packaged.is_dir():
262 return packaged
263 repository_root = Path(__file__).resolve().parents[1]
264 if (repository_root / "setup.py").is_file():
265 source_build = (
266 repository_root / "build" / "native" / "payload" / "hyper_multicore" / "lib"
267 )
268 if source_build.is_dir():
269 return source_build
270 return packaged
273def _environment_contains_path(variable: str, expected: Path) -> bool:
274 """Return whether an environment path list contains ``expected``."""
275 configured = [value for value in os.environ.get(variable, "").split(os.pathsep) if value]
276 return str(expected.resolve()) in {str(Path(value).resolve()) for value in configured}