mblt-npu-python 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mblt_npu/__init__.py +36 -0
- mblt_npu/logging.py +54 -0
- mblt_npu/npu_backend.py +632 -0
- mblt_npu/onnx_backend.py +90 -0
- mblt_npu/pytest_plugin.py +218 -0
- mblt_npu_python-0.0.0.dist-info/METADATA +121 -0
- mblt_npu_python-0.0.0.dist-info/RECORD +10 -0
- mblt_npu_python-0.0.0.dist-info/WHEEL +5 -0
- mblt_npu_python-0.0.0.dist-info/licenses/LICENSE +28 -0
- mblt_npu_python-0.0.0.dist-info/top_level.txt +1 -0
mblt_npu/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Shared NPU access for the mblt Python packages.
|
|
2
|
+
|
|
3
|
+
`MobilintNPUBackend` is defined exactly once, here, because the alternative
|
|
4
|
+
was a copy in each of mblt-vision-python, mblt-transformers-python and
|
|
5
|
+
mblt-MeloTTS-python that nothing would keep in sync.
|
|
6
|
+
|
|
7
|
+
`logging` lives here too rather than in vision, its only caller, because the
|
|
8
|
+
two are mutually dependent: npu_backend imports log_model_details and
|
|
9
|
+
log_model_details reads a MobilintNPUBackend's fields.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .npu_backend import (
|
|
13
|
+
BACKEND_CLASSES,
|
|
14
|
+
DEFAULT_TARGET_DEVICE,
|
|
15
|
+
MobilintAriesBackend,
|
|
16
|
+
MobilintNPUBackend,
|
|
17
|
+
MobilintRegulusBackend,
|
|
18
|
+
backend_class_for,
|
|
19
|
+
normalize_target_device,
|
|
20
|
+
)
|
|
21
|
+
from .logging import log_model_details
|
|
22
|
+
from .onnx_backend import ONNXBackend
|
|
23
|
+
|
|
24
|
+
__version__ = "0.0.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BACKEND_CLASSES",
|
|
28
|
+
"DEFAULT_TARGET_DEVICE",
|
|
29
|
+
"MobilintAriesBackend",
|
|
30
|
+
"MobilintNPUBackend",
|
|
31
|
+
"MobilintRegulusBackend",
|
|
32
|
+
"ONNXBackend",
|
|
33
|
+
"backend_class_for",
|
|
34
|
+
"log_model_details",
|
|
35
|
+
"normalize_target_device",
|
|
36
|
+
]
|
mblt_npu/logging.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .npu_backend import MobilintNPUBackend
|
|
7
|
+
|
|
8
|
+
_VERBOSE_TRUE_VALUES = {"1", "true", "yes", "on"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_verbose_enabled() -> bool:
|
|
12
|
+
return os.getenv("MBLT_MODEL_ZOO_VERBOSE", "").lower() in _VERBOSE_TRUE_VALUES
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _md5_hash_from_file(file_path: str) -> str:
|
|
16
|
+
hash_md5 = hashlib.md5()
|
|
17
|
+
with open(file_path, "rb") as file_handle:
|
|
18
|
+
for chunk in iter(lambda: file_handle.read(8192), b""):
|
|
19
|
+
hash_md5.update(chunk)
|
|
20
|
+
return hash_md5.hexdigest()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def log_model_details(
|
|
24
|
+
model_path: str, npu_backend: Optional["MobilintNPUBackend"] = None
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Print model metadata when verbose logging is enabled."""
|
|
27
|
+
if not _is_verbose_enabled():
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
print("Model Initialized")
|
|
31
|
+
print(f"Model Size: {os.path.getsize(model_path) / 1024 / 1024:.2f} MB")
|
|
32
|
+
print(f"Model Hash: {_md5_hash_from_file(model_path)}")
|
|
33
|
+
|
|
34
|
+
if npu_backend is not None:
|
|
35
|
+
print(f"Device Number: {npu_backend.dev_no}")
|
|
36
|
+
print(f"Core Mode: {npu_backend.core_mode}")
|
|
37
|
+
if npu_backend.core_mode == "single":
|
|
38
|
+
print(f"Target Cores: {npu_backend.target_cores}")
|
|
39
|
+
else:
|
|
40
|
+
print(f"Target Clusters: {npu_backend.target_clusters}")
|
|
41
|
+
if npu_backend.mxq_model.get_num_model_variants() == 1:
|
|
42
|
+
print(f"Model Input Shape: {npu_backend.mxq_model.get_model_input_shape()}")
|
|
43
|
+
print(
|
|
44
|
+
f"Model Output Shape: {npu_backend.mxq_model.get_model_output_shape()}"
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
for i in range(npu_backend.mxq_model.get_num_model_variants()):
|
|
48
|
+
print(f"Model Variant {i}")
|
|
49
|
+
print(
|
|
50
|
+
f"\tInput Shape: {npu_backend.mxq_model.get_model_variant_handle(i).get_model_input_shape()}"
|
|
51
|
+
)
|
|
52
|
+
print(
|
|
53
|
+
f"\tOutput Shape: {npu_backend.mxq_model.get_model_variant_handle(i).get_model_output_shape()}"
|
|
54
|
+
)
|
mblt_npu/npu_backend.py
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Dict, List, Literal, Optional, Sequence, Union
|
|
4
|
+
|
|
5
|
+
from huggingface_hub import HfApi, hf_hub_download
|
|
6
|
+
from huggingface_hub.errors import EntryNotFoundError
|
|
7
|
+
from qbruntime import Accelerator, Cluster, Core, CoreId, Model, ModelConfig
|
|
8
|
+
|
|
9
|
+
from .logging import log_model_details
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _enum_value(value: Any) -> int:
|
|
15
|
+
"""Return an integer enum value across qbruntime binding versions."""
|
|
16
|
+
|
|
17
|
+
while hasattr(value, "value"):
|
|
18
|
+
value = value.value
|
|
19
|
+
return int(value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# qbruntime has shipped both a Python enum wrapping a native enum and a direct
|
|
23
|
+
# enum binding. Peel every ``.value`` layer so serialized fields are stable
|
|
24
|
+
# integers across both forms (Cluster0 is 65536; Core0 is 1).
|
|
25
|
+
cluster_map = {
|
|
26
|
+
_enum_value(cluster): cluster for cluster in (Cluster.Cluster0, Cluster.Cluster1)
|
|
27
|
+
}
|
|
28
|
+
core_map = {
|
|
29
|
+
_enum_value(core): core for core in (Core.Core0, Core.Core1, Core.Core2, Core.Core3)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
DEFAULT_TARGET_DEVICE = "aries-rb"
|
|
33
|
+
"""Default supported Mobilint NPU board."""
|
|
34
|
+
|
|
35
|
+
_TARGET_DEVICE_ALIASES = {
|
|
36
|
+
# Configurations written before board-specific target devices were exposed
|
|
37
|
+
# used these generic product names. Keep them readable, but always serialize
|
|
38
|
+
# one of the supported board identifiers below.
|
|
39
|
+
"aries": "aries-rb",
|
|
40
|
+
"regulus": "regulus-ra",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def normalize_target_device(target_device: str) -> str:
|
|
45
|
+
"""Return a supported board identifier, accepting legacy generic product names."""
|
|
46
|
+
|
|
47
|
+
if not isinstance(target_device, str):
|
|
48
|
+
raise TypeError(
|
|
49
|
+
f"target_device must be a string, got {type(target_device).__name__}."
|
|
50
|
+
)
|
|
51
|
+
return _TARGET_DEVICE_ALIASES.get(target_device.lower(), target_device.lower())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MobilintNPUBackend:
|
|
55
|
+
"""Shared NPU access. Subclassed per product, because core allocation differs.
|
|
56
|
+
|
|
57
|
+
The SDK exposes every core-mode setter on `ModelConfig` regardless of product
|
|
58
|
+
and offers no capability query, so "does this device support global8" is only
|
|
59
|
+
answerable by trying it and reading a load failure. Aries has two clusters of
|
|
60
|
+
four cores and four modes; Regulus has one core and only single. Encoding that
|
|
61
|
+
in types is what turns a late `StatusCode(16)` into an argument error.
|
|
62
|
+
|
|
63
|
+
Instantiating this class directly selects a backend from ``target_device``.
|
|
64
|
+
The default is ``aries-rb``; ``regulus-ra`` and ``regulus-rb`` select the
|
|
65
|
+
Regulus implementation. Existing generic ``aries`` and ``regulus`` config
|
|
66
|
+
values remain accepted as input compatibility aliases.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Aries geometry. Overridden per product.
|
|
70
|
+
num_of_clusters = 2
|
|
71
|
+
num_of_cores_in_cluster = 4
|
|
72
|
+
|
|
73
|
+
#: Core modes this product accepts. Empty on the base class, which never runs.
|
|
74
|
+
supported_core_modes: tuple = ()
|
|
75
|
+
#: Default board when this subclass is instantiated directly.
|
|
76
|
+
default_target_device: str = DEFAULT_TARGET_DEVICE
|
|
77
|
+
#: Board identifiers accepted by this backend implementation.
|
|
78
|
+
supported_target_devices: tuple[str, ...] = ()
|
|
79
|
+
|
|
80
|
+
def __new__(cls, *args, **kwargs):
|
|
81
|
+
# MobilintNPUBackend(...) remains the public construction point. The
|
|
82
|
+
# board identifier picks its implementation.
|
|
83
|
+
if cls is MobilintNPUBackend:
|
|
84
|
+
# ``target_device`` is the eighth positional parameter of
|
|
85
|
+
# ``__init__`` as well as a keyword argument. Respect either form
|
|
86
|
+
# before selecting the product-specific implementation.
|
|
87
|
+
requested_device = kwargs.get("target_device")
|
|
88
|
+
if requested_device is None and len(args) > 7:
|
|
89
|
+
requested_device = args[7]
|
|
90
|
+
requested_device = requested_device or DEFAULT_TARGET_DEVICE
|
|
91
|
+
device = normalize_target_device(requested_device)
|
|
92
|
+
cls = backend_class_for(device)
|
|
93
|
+
return super().__new__(cls)
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
mxq_path: str = "",
|
|
98
|
+
dev_no: int = 0,
|
|
99
|
+
core_mode: Literal["auto", "single", "multi", "global4", "global8"] = "single",
|
|
100
|
+
target_cores: Optional[List[Union[str, "CoreId"]]] = None,
|
|
101
|
+
target_clusters: Optional[Sequence[Union[int, "Cluster"]]] = None,
|
|
102
|
+
revision: Optional[str] = None,
|
|
103
|
+
commit_hash: Optional[str] = None,
|
|
104
|
+
target_device: str | None = None,
|
|
105
|
+
**kwargs,
|
|
106
|
+
):
|
|
107
|
+
resolved_target_device = normalize_target_device(
|
|
108
|
+
target_device or self.default_target_device
|
|
109
|
+
)
|
|
110
|
+
if resolved_target_device not in self.supported_target_devices:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"target_device {resolved_target_device!r} is not supported by "
|
|
113
|
+
f"{type(self).__name__}; expected one of "
|
|
114
|
+
f"{', '.join(self.supported_target_devices)}."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# ``from_dict`` supplies a serialized repository name through ``kwargs``.
|
|
118
|
+
# Keep it so an MXQ path can still be resolved from the Hub after a
|
|
119
|
+
# backend round trip; model mixins may replace it later when applicable.
|
|
120
|
+
self.name_or_path: str = kwargs.get("name_or_path", "")
|
|
121
|
+
self.target_device = resolved_target_device
|
|
122
|
+
self.revision = revision
|
|
123
|
+
self._commit_hash = commit_hash
|
|
124
|
+
self.mxq_path = mxq_path
|
|
125
|
+
self.dev_no = dev_no
|
|
126
|
+
self.core_mode = core_mode
|
|
127
|
+
if core_mode not in self.supported_core_modes:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"core_mode {core_mode!r} is not available on "
|
|
130
|
+
f"{self.target_device}; supported: "
|
|
131
|
+
f"{', '.join(self.supported_core_modes)}. Refusing here rather than "
|
|
132
|
+
"letting the SDK fail at Model::create, which is where an "
|
|
133
|
+
"unsupported mode surfaces otherwise."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self._target_cores_serialized: List[str] = []
|
|
137
|
+
self.target_cores = target_cores if target_cores is not None else []
|
|
138
|
+
|
|
139
|
+
self._target_clusters_serialized: List[str] = []
|
|
140
|
+
self.target_clusters = target_clusters if target_clusters is not None else []
|
|
141
|
+
|
|
142
|
+
def check_model_path(self, mxq_path: str) -> str:
|
|
143
|
+
# 1. current relative/absolute path
|
|
144
|
+
if os.path.exists(mxq_path):
|
|
145
|
+
return mxq_path
|
|
146
|
+
|
|
147
|
+
# 2. inside the local path
|
|
148
|
+
if os.path.isdir(self.name_or_path):
|
|
149
|
+
local_path = os.path.join(self.name_or_path, mxq_path)
|
|
150
|
+
if os.path.exists(local_path):
|
|
151
|
+
return local_path
|
|
152
|
+
|
|
153
|
+
# 3. If none of above, download mxq file from hub
|
|
154
|
+
else:
|
|
155
|
+
name_or_path = (
|
|
156
|
+
self.name_or_path
|
|
157
|
+
if self.name_or_path.startswith("mobilint/")
|
|
158
|
+
else "mobilint/" + self.name_or_path
|
|
159
|
+
)
|
|
160
|
+
revision = (
|
|
161
|
+
getattr(self, "revision", None)
|
|
162
|
+
or getattr(self, "_commit_hash", None)
|
|
163
|
+
or self._infer_hf_revision_from_cache(name_or_path)
|
|
164
|
+
)
|
|
165
|
+
try:
|
|
166
|
+
return hf_hub_download(
|
|
167
|
+
repo_id=name_or_path,
|
|
168
|
+
filename=mxq_path,
|
|
169
|
+
revision=revision,
|
|
170
|
+
)
|
|
171
|
+
except EntryNotFoundError:
|
|
172
|
+
try:
|
|
173
|
+
return hf_hub_download(
|
|
174
|
+
repo_id=name_or_path,
|
|
175
|
+
filename=mxq_path,
|
|
176
|
+
revision=revision,
|
|
177
|
+
)
|
|
178
|
+
except EntryNotFoundError:
|
|
179
|
+
cached = self._find_cached_mxq(name_or_path, mxq_path, revision)
|
|
180
|
+
if cached is not None:
|
|
181
|
+
return cached
|
|
182
|
+
mxq_candidate = self._find_mxq_from_hub(
|
|
183
|
+
name_or_path, mxq_path, revision
|
|
184
|
+
)
|
|
185
|
+
if mxq_candidate is None:
|
|
186
|
+
raise
|
|
187
|
+
return hf_hub_download(
|
|
188
|
+
repo_id=name_or_path,
|
|
189
|
+
filename=mxq_candidate,
|
|
190
|
+
revision=revision,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
raise Exception(f"[Mobilint] Error: Could not locate {mxq_path}.")
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _infer_hf_revision_from_cache(repo_id: str) -> Optional[str]:
|
|
197
|
+
if not repo_id or "/" not in repo_id:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
cache_root = os.getenv("HUGGINGFACE_HUB_CACHE") or os.getenv("HF_HUB_CACHE")
|
|
201
|
+
if not cache_root:
|
|
202
|
+
hf_home = os.getenv("HF_HOME") or os.path.join(
|
|
203
|
+
os.path.expanduser("~"),
|
|
204
|
+
".cache",
|
|
205
|
+
"huggingface",
|
|
206
|
+
)
|
|
207
|
+
cache_root = os.path.join(hf_home, "hub")
|
|
208
|
+
|
|
209
|
+
repo_dir = os.path.join(cache_root, f"models--{repo_id.replace('/', '--')}")
|
|
210
|
+
refs_dir = os.path.join(repo_dir, "refs")
|
|
211
|
+
if os.path.isdir(refs_dir):
|
|
212
|
+
for ref_name in ("main", "master"):
|
|
213
|
+
ref_path = os.path.join(refs_dir, ref_name)
|
|
214
|
+
if os.path.isfile(ref_path):
|
|
215
|
+
try:
|
|
216
|
+
with open(ref_path, "r", encoding="utf-8") as f:
|
|
217
|
+
ref = f.read().strip()
|
|
218
|
+
if ref:
|
|
219
|
+
return ref
|
|
220
|
+
except OSError:
|
|
221
|
+
pass
|
|
222
|
+
try:
|
|
223
|
+
for entry in os.listdir(refs_dir):
|
|
224
|
+
ref_path = os.path.join(refs_dir, entry)
|
|
225
|
+
if os.path.isfile(ref_path):
|
|
226
|
+
with open(ref_path, "r", encoding="utf-8") as f:
|
|
227
|
+
ref = f.read().strip()
|
|
228
|
+
if ref:
|
|
229
|
+
return ref
|
|
230
|
+
except OSError:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
snapshots_dir = os.path.join(repo_dir, "snapshots")
|
|
234
|
+
if os.path.isdir(snapshots_dir):
|
|
235
|
+
try:
|
|
236
|
+
for entry in os.listdir(snapshots_dir):
|
|
237
|
+
if os.path.isdir(os.path.join(snapshots_dir, entry)):
|
|
238
|
+
return entry
|
|
239
|
+
except OSError:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _find_cached_mxq(
|
|
246
|
+
repo_id: str, mxq_path: str, revision: Optional[str] = None
|
|
247
|
+
) -> Optional[str]:
|
|
248
|
+
if not repo_id or "/" not in repo_id:
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
cache_root = os.getenv("HUGGINGFACE_HUB_CACHE") or os.getenv("HF_HUB_CACHE")
|
|
252
|
+
if not cache_root:
|
|
253
|
+
hf_home = os.getenv("HF_HOME") or os.path.join(
|
|
254
|
+
os.path.expanduser("~"),
|
|
255
|
+
".cache",
|
|
256
|
+
"huggingface",
|
|
257
|
+
)
|
|
258
|
+
cache_root = os.path.join(hf_home, "hub")
|
|
259
|
+
|
|
260
|
+
repo_dir = os.path.join(cache_root, f"models--{repo_id.replace('/', '--')}")
|
|
261
|
+
snapshots_dir = os.path.join(repo_dir, "snapshots")
|
|
262
|
+
if not os.path.isdir(snapshots_dir):
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
rel_candidates = [mxq_path, os.path.basename(mxq_path)]
|
|
266
|
+
try:
|
|
267
|
+
snapshots = os.listdir(snapshots_dir)
|
|
268
|
+
if revision is not None:
|
|
269
|
+
snapshots = [snapshot for snapshot in snapshots if snapshot == revision]
|
|
270
|
+
for snapshot in snapshots:
|
|
271
|
+
snapshot_dir = os.path.join(snapshots_dir, snapshot)
|
|
272
|
+
if not os.path.isdir(snapshot_dir):
|
|
273
|
+
continue
|
|
274
|
+
for rel in rel_candidates:
|
|
275
|
+
candidate = os.path.join(snapshot_dir, rel)
|
|
276
|
+
if os.path.isfile(candidate):
|
|
277
|
+
return candidate
|
|
278
|
+
except OSError:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def _find_mxq_from_hub(
|
|
285
|
+
repo_id: str, mxq_path: str, revision: Optional[str] = None
|
|
286
|
+
) -> Optional[str]:
|
|
287
|
+
try:
|
|
288
|
+
files = HfApi().list_repo_files(repo_id=repo_id, revision=revision)
|
|
289
|
+
except Exception:
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
basename = os.path.basename(mxq_path)
|
|
293
|
+
if basename in files:
|
|
294
|
+
return basename
|
|
295
|
+
if mxq_path in files:
|
|
296
|
+
return mxq_path
|
|
297
|
+
|
|
298
|
+
raise ValueError(
|
|
299
|
+
f"Cannot find {mxq_path} file from HuggingFace repo: f{repo_id}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def _configure_core_mode(self, mc: "ModelConfig") -> None:
|
|
303
|
+
raise NotImplementedError(
|
|
304
|
+
"MobilintNPUBackend is a base class; use MobilintAriesBackend, "
|
|
305
|
+
"MobilintRegulusBackend, or backend_class_for(target_device)."
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def create(self):
|
|
309
|
+
self.acc = Accelerator(self.dev_no)
|
|
310
|
+
mc = ModelConfig()
|
|
311
|
+
self._configure_core_mode(mc)
|
|
312
|
+
|
|
313
|
+
model_path = self.check_model_path(self.mxq_path)
|
|
314
|
+
self.mxq_model = Model(model_path, mc)
|
|
315
|
+
log_model_details(model_path, self)
|
|
316
|
+
|
|
317
|
+
def launch(self):
|
|
318
|
+
self.mxq_model.launch(self.acc)
|
|
319
|
+
|
|
320
|
+
def __call__(self, x: Any) -> Any:
|
|
321
|
+
"""Run inference with the loaded MXQ model.
|
|
322
|
+
|
|
323
|
+
This compatibility entry point is used by the Vision engine and mirrors
|
|
324
|
+
the historical Model Zoo backend contract.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
return self.mxq_model.infer(x)
|
|
328
|
+
|
|
329
|
+
def get_dtype(self) -> str:
|
|
330
|
+
"""Return the loaded model input data type as a runtime string."""
|
|
331
|
+
|
|
332
|
+
return str(self.mxq_model.get_model_input_data_type())
|
|
333
|
+
|
|
334
|
+
def dispose(self):
|
|
335
|
+
self.mxq_model.dispose()
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def target_cores(self) -> List["CoreId"]:
|
|
339
|
+
result = []
|
|
340
|
+
if not hasattr(self, "_target_cores_serialized"):
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
for s in self._target_cores_serialized:
|
|
344
|
+
try:
|
|
345
|
+
c_val, r_val = map(int, s.split(":"))
|
|
346
|
+
if c_val in (0, 1):
|
|
347
|
+
cluster = (Cluster.Cluster0, Cluster.Cluster1)[c_val]
|
|
348
|
+
core = (Core.Core0, Core.Core1, Core.Core2, Core.Core3)[r_val]
|
|
349
|
+
else:
|
|
350
|
+
cluster = cluster_map[c_val]
|
|
351
|
+
core = core_map[r_val]
|
|
352
|
+
core_id_factory: Any = CoreId
|
|
353
|
+
try:
|
|
354
|
+
# qbruntime's current binding exposes a no-argument
|
|
355
|
+
# constructor, although older type stubs declare only the
|
|
356
|
+
# legacy two-argument form.
|
|
357
|
+
core_id = core_id_factory()
|
|
358
|
+
core_id.cluster = cluster
|
|
359
|
+
core_id.core = core
|
|
360
|
+
except TypeError:
|
|
361
|
+
core_id = CoreId(cluster, core)
|
|
362
|
+
result.append(core_id)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
# Raising rather than warning-and-skipping: this used to drop the
|
|
365
|
+
# entry and return a shorter list, so a caller who asked for two
|
|
366
|
+
# specific cores silently got none and ran on whatever the default
|
|
367
|
+
# allocation gave them.
|
|
368
|
+
raise ValueError(
|
|
369
|
+
f"cannot deserialize target core {s!r}: expected "
|
|
370
|
+
f'"<cluster value>:<core value>" with cluster in '
|
|
371
|
+
f"{sorted(cluster_map)} and core in {sorted(core_map)}"
|
|
372
|
+
) from e
|
|
373
|
+
return result
|
|
374
|
+
|
|
375
|
+
@target_cores.setter
|
|
376
|
+
def target_cores(self, values: List[Union[str, "CoreId"]]):
|
|
377
|
+
serialized = []
|
|
378
|
+
for v in values:
|
|
379
|
+
if isinstance(v, CoreId):
|
|
380
|
+
cluster_index = next(
|
|
381
|
+
index
|
|
382
|
+
for index, cluster in enumerate(
|
|
383
|
+
(Cluster.Cluster0, Cluster.Cluster1)
|
|
384
|
+
)
|
|
385
|
+
if _enum_value(cluster) == _enum_value(v.cluster)
|
|
386
|
+
)
|
|
387
|
+
core_index = next(
|
|
388
|
+
index
|
|
389
|
+
for index, core in enumerate(
|
|
390
|
+
(Core.Core0, Core.Core1, Core.Core2, Core.Core3)
|
|
391
|
+
)
|
|
392
|
+
if _enum_value(core) == _enum_value(v.core)
|
|
393
|
+
)
|
|
394
|
+
serialized.append(f"{cluster_index}:{core_index}")
|
|
395
|
+
elif isinstance(v, str):
|
|
396
|
+
if ":" in v:
|
|
397
|
+
serialized.append(v)
|
|
398
|
+
else:
|
|
399
|
+
raise ValueError(f"Invalid format: {v}")
|
|
400
|
+
else:
|
|
401
|
+
raise TypeError(f"Unsupported type: {type(v)}")
|
|
402
|
+
|
|
403
|
+
self._target_cores_serialized = serialized
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def target_clusters(self) -> List["Cluster"]:
|
|
407
|
+
result = []
|
|
408
|
+
if not hasattr(self, "_target_clusters_serialized"):
|
|
409
|
+
return []
|
|
410
|
+
|
|
411
|
+
for s in self._target_clusters_serialized:
|
|
412
|
+
try:
|
|
413
|
+
c_val = int(s)
|
|
414
|
+
result.append(
|
|
415
|
+
(Cluster.Cluster0, Cluster.Cluster1)[c_val]
|
|
416
|
+
if c_val in (0, 1)
|
|
417
|
+
else cluster_map[c_val]
|
|
418
|
+
)
|
|
419
|
+
except Exception as e:
|
|
420
|
+
raise ValueError(
|
|
421
|
+
f"cannot deserialize target cluster {s!r}: expected one of "
|
|
422
|
+
f"{sorted(cluster_map)}"
|
|
423
|
+
) from e
|
|
424
|
+
return result
|
|
425
|
+
|
|
426
|
+
@target_clusters.setter
|
|
427
|
+
def target_clusters(self, values: Sequence[Union[int, "Cluster"]]):
|
|
428
|
+
serialized = []
|
|
429
|
+
for v in values:
|
|
430
|
+
if isinstance(v, Cluster):
|
|
431
|
+
serialized.append(
|
|
432
|
+
next(
|
|
433
|
+
index
|
|
434
|
+
for index, cluster in enumerate(
|
|
435
|
+
(Cluster.Cluster0, Cluster.Cluster1)
|
|
436
|
+
)
|
|
437
|
+
if _enum_value(cluster) == _enum_value(v)
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
elif isinstance(v, int):
|
|
441
|
+
# Callers pass 0 and 1 meaning "first cluster", "second cluster".
|
|
442
|
+
# Preserve this public serialized form while accepting native enum
|
|
443
|
+
# values from older callers as well.
|
|
444
|
+
ordinals = [Cluster.Cluster0, Cluster.Cluster1]
|
|
445
|
+
if 0 <= v < len(ordinals):
|
|
446
|
+
serialized.append(v)
|
|
447
|
+
elif v in cluster_map:
|
|
448
|
+
serialized.append(v)
|
|
449
|
+
else:
|
|
450
|
+
raise ValueError(
|
|
451
|
+
f"cluster {v} is neither an index into "
|
|
452
|
+
f"{[c.name for c in ordinals]} nor one of {sorted(cluster_map)}"
|
|
453
|
+
)
|
|
454
|
+
else:
|
|
455
|
+
raise TypeError(f"Unsupported type: {type(v)}")
|
|
456
|
+
|
|
457
|
+
self._target_clusters_serialized = serialized
|
|
458
|
+
|
|
459
|
+
def to_dict(self, prefix="") -> Dict[str, Any]:
|
|
460
|
+
p = prefix
|
|
461
|
+
result = {
|
|
462
|
+
"name_or_path": self.name_or_path,
|
|
463
|
+
f"{p}mxq_path": self.mxq_path,
|
|
464
|
+
f"{p}dev_no": self.dev_no,
|
|
465
|
+
f"{p}core_mode": self.core_mode,
|
|
466
|
+
f"{p}revision": self.revision,
|
|
467
|
+
f"{p}commit_hash": self._commit_hash,
|
|
468
|
+
f"{p}target_device": self.target_device,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if self.core_mode == "single":
|
|
472
|
+
result[f"{p}target_cores"] = self._target_cores_serialized
|
|
473
|
+
else:
|
|
474
|
+
result[f"{p}target_clusters"] = self._target_clusters_serialized
|
|
475
|
+
|
|
476
|
+
return result
|
|
477
|
+
|
|
478
|
+
@classmethod
|
|
479
|
+
def from_dict(cls, data: Dict[str, Any], prefix: str = "") -> "MobilintNPUBackend":
|
|
480
|
+
"""Rebuilds a backend from a flat config dict.
|
|
481
|
+
|
|
482
|
+
Called as `MobilintNPUBackend.from_dict(...)`, `cls` is the base and
|
|
483
|
+
`__new__` picks the subclass from `target_device`. Called on a subclass,
|
|
484
|
+
a serialized board supported by that subclass is retained; a board for a
|
|
485
|
+
different backend falls back to the subclass default.
|
|
486
|
+
"""
|
|
487
|
+
p = prefix
|
|
488
|
+
data = dict(data)
|
|
489
|
+
if cls is not MobilintNPUBackend:
|
|
490
|
+
serialized_target_device = data.get(f"{prefix}target_device")
|
|
491
|
+
if serialized_target_device is None:
|
|
492
|
+
data[f"{prefix}target_device"] = cls.default_target_device
|
|
493
|
+
else:
|
|
494
|
+
target_device = normalize_target_device(serialized_target_device)
|
|
495
|
+
data[f"{prefix}target_device"] = (
|
|
496
|
+
target_device
|
|
497
|
+
if target_device in cls.supported_target_devices
|
|
498
|
+
else cls.default_target_device
|
|
499
|
+
)
|
|
500
|
+
if f"{p}target_cores" in data.keys() and f"{p}target_clusters" in data.keys():
|
|
501
|
+
logger.warning(f"{p}target_cores and {p}target_clusters are both set!")
|
|
502
|
+
logger.warning(
|
|
503
|
+
f"If {p}core_mode is `single`, only {p}target_cores will be used."
|
|
504
|
+
)
|
|
505
|
+
logger.warning(
|
|
506
|
+
f"If {p}core_mode is `multi`, `global4`, or `global8`, only {p}target_clusters will be used."
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return cls(
|
|
510
|
+
name_or_path=data.pop("name_or_path", ""),
|
|
511
|
+
mxq_path=data.pop(f"{p}mxq_path", ""),
|
|
512
|
+
dev_no=data.pop(f"{p}dev_no", 0),
|
|
513
|
+
core_mode=data.pop(f"{p}core_mode", "single"),
|
|
514
|
+
target_cores=data.pop(f"{p}target_cores", None),
|
|
515
|
+
target_clusters=data.pop(f"{p}target_clusters", None),
|
|
516
|
+
revision=data.pop(f"{p}revision", None),
|
|
517
|
+
commit_hash=data.pop(f"{p}commit_hash", None),
|
|
518
|
+
target_device=data.pop(f"{p}target_device", None),
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class MobilintAriesBackend(MobilintNPUBackend):
|
|
523
|
+
"""Aries: two clusters of four cores, four allocation modes plus auto."""
|
|
524
|
+
|
|
525
|
+
num_of_clusters = 2
|
|
526
|
+
num_of_cores_in_cluster = 4
|
|
527
|
+
supported_core_modes = ("auto", "single", "multi", "global4", "global8")
|
|
528
|
+
default_target_device = "aries-rb"
|
|
529
|
+
supported_target_devices = ("aries-rb",)
|
|
530
|
+
|
|
531
|
+
def _configure_core_mode(self, mc: "ModelConfig") -> None:
|
|
532
|
+
if self.core_mode == "auto":
|
|
533
|
+
# What an unconfigured ModelConfig already is. Named explicitly because
|
|
534
|
+
# the previous code reached it by doing nothing under the label
|
|
535
|
+
# "single", and measured, the default is CoreMode.Auto with
|
|
536
|
+
# num_cores=0 — not single with all cores as its comment claimed.
|
|
537
|
+
mc.set_auto_core_mode()
|
|
538
|
+
elif self.core_mode == "single":
|
|
539
|
+
cores = self.target_cores
|
|
540
|
+
if cores:
|
|
541
|
+
mc.set_single_core_mode(core_ids=cores)
|
|
542
|
+
else:
|
|
543
|
+
# With no explicit cores, let qbruntime allocate one local core.
|
|
544
|
+
mc.set_single_core_mode(1)
|
|
545
|
+
elif self.core_mode == "multi":
|
|
546
|
+
mc.set_multi_core_mode(self.target_clusters)
|
|
547
|
+
elif self.core_mode == "global4":
|
|
548
|
+
mc.set_global4_core_mode(self.target_clusters)
|
|
549
|
+
elif self.core_mode == "global8":
|
|
550
|
+
clusters = self.target_clusters
|
|
551
|
+
expected_clusters = {
|
|
552
|
+
_enum_value(Cluster.Cluster0),
|
|
553
|
+
_enum_value(Cluster.Cluster1),
|
|
554
|
+
}
|
|
555
|
+
if (
|
|
556
|
+
len(clusters) != len(expected_clusters)
|
|
557
|
+
or {_enum_value(cluster) for cluster in clusters} != expected_clusters
|
|
558
|
+
):
|
|
559
|
+
raise ValueError(
|
|
560
|
+
"global8 requires target_clusters to select both Aries clusters."
|
|
561
|
+
)
|
|
562
|
+
mc.set_global8_core_mode()
|
|
563
|
+
else: # unreachable: __init__ validates against supported_core_modes
|
|
564
|
+
raise ValueError(f"unhandled core_mode {self.core_mode!r}")
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
class MobilintRegulusBackend(MobilintNPUBackend):
|
|
568
|
+
"""Regulus: one core, single only.
|
|
569
|
+
|
|
570
|
+
Measured on a board rather than inferred: `global4` is rejected by qbruntime
|
|
571
|
+
and `global8` fails Model::create with StatusCode(16), while `auto` and
|
|
572
|
+
`single` both run. Cluster arguments have nothing to address, so they are
|
|
573
|
+
refused instead of ignored — ignoring them would let a caller believe an
|
|
574
|
+
allocation happened.
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
num_of_clusters = 1
|
|
578
|
+
num_of_cores_in_cluster = 1
|
|
579
|
+
supported_core_modes = ("auto", "single")
|
|
580
|
+
default_target_device = "regulus-ra"
|
|
581
|
+
supported_target_devices = ("regulus-ra", "regulus-rb")
|
|
582
|
+
|
|
583
|
+
def __init__(self, *args, **kwargs):
|
|
584
|
+
super().__init__(*args, **kwargs)
|
|
585
|
+
if self.target_clusters:
|
|
586
|
+
raise ValueError(
|
|
587
|
+
"target_clusters is meaningless on regulus, which has a single "
|
|
588
|
+
f"core: got {self._target_clusters_serialized}. Remove it, or use "
|
|
589
|
+
"target_device='aries-rb'."
|
|
590
|
+
)
|
|
591
|
+
cores = self.target_cores
|
|
592
|
+
expected_cluster = _enum_value(Cluster.Cluster0)
|
|
593
|
+
expected_core = _enum_value(Core.Core0)
|
|
594
|
+
if len(cores) > 1 or any(
|
|
595
|
+
_enum_value(core.cluster) != expected_cluster
|
|
596
|
+
or _enum_value(core.core) != expected_core
|
|
597
|
+
for core in cores
|
|
598
|
+
):
|
|
599
|
+
raise ValueError(
|
|
600
|
+
"target_cores on regulus may select only its sole core (0:0)."
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
def _configure_core_mode(self, mc: "ModelConfig") -> None:
|
|
604
|
+
if self.core_mode == "auto":
|
|
605
|
+
mc.set_auto_core_mode()
|
|
606
|
+
else:
|
|
607
|
+
cores = self.target_cores
|
|
608
|
+
if cores:
|
|
609
|
+
mc.set_single_core_mode(core_ids=cores)
|
|
610
|
+
else:
|
|
611
|
+
mc.set_single_core_mode(1)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
#: target_device -> backend class. A mapping rather than a chain of ifs so that
|
|
615
|
+
#: adding a product is one entry and an unknown one is an error naming the
|
|
616
|
+
#: choices, instead of silently behaving like Aries.
|
|
617
|
+
BACKEND_CLASSES = {
|
|
618
|
+
"aries-rb": MobilintAriesBackend,
|
|
619
|
+
"regulus-ra": MobilintRegulusBackend,
|
|
620
|
+
"regulus-rb": MobilintRegulusBackend,
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def backend_class_for(target_device: str):
|
|
625
|
+
normalized_target_device = normalize_target_device(target_device)
|
|
626
|
+
try:
|
|
627
|
+
return BACKEND_CLASSES[normalized_target_device]
|
|
628
|
+
except KeyError:
|
|
629
|
+
raise ValueError(
|
|
630
|
+
f"unknown target_device {target_device!r}; "
|
|
631
|
+
f"expected one of {', '.join(sorted(BACKEND_CLASSES))}"
|
|
632
|
+
) from None
|
mblt_npu/onnx_backend.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Optional ONNX Runtime backend shared by Mobilint Python packages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ONNXBackend:
|
|
11
|
+
"""Run an ONNX model through an optional ONNX Runtime installation.
|
|
12
|
+
|
|
13
|
+
``onnxruntime`` is imported only by :meth:`create`, keeping the base NPU
|
|
14
|
+
package usable for MXQ-only applications.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
model_path: str,
|
|
20
|
+
*,
|
|
21
|
+
providers: Sequence[str] | None = None,
|
|
22
|
+
ort_module: Any | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.model_path = model_path
|
|
25
|
+
self.providers = (
|
|
26
|
+
list(providers) if providers is not None else ["CPUExecutionProvider"]
|
|
27
|
+
)
|
|
28
|
+
self._ort_module = ort_module
|
|
29
|
+
self.session: Any | None = None
|
|
30
|
+
|
|
31
|
+
def _load_onnxruntime(self) -> Any:
|
|
32
|
+
if self._ort_module is not None:
|
|
33
|
+
return self._ort_module
|
|
34
|
+
try:
|
|
35
|
+
import onnxruntime
|
|
36
|
+
except ImportError as exc:
|
|
37
|
+
raise ImportError(
|
|
38
|
+
"onnxruntime is required for ONNX inference. "
|
|
39
|
+
"Install it with `pip install mblt-npu-python[onnxruntime]`."
|
|
40
|
+
) from exc
|
|
41
|
+
return onnxruntime
|
|
42
|
+
|
|
43
|
+
def create(self) -> None:
|
|
44
|
+
"""Create the ONNX Runtime inference session for ``model_path``."""
|
|
45
|
+
|
|
46
|
+
if not os.path.isfile(self.model_path):
|
|
47
|
+
raise FileNotFoundError(f"ONNX file not found at: {self.model_path}")
|
|
48
|
+
self.session = self._load_onnxruntime().InferenceSession(
|
|
49
|
+
self.model_path, providers=self.providers
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def launch(self) -> None:
|
|
53
|
+
"""Match the MXQ backend lifecycle; ONNX Runtime needs no launch step."""
|
|
54
|
+
|
|
55
|
+
self._require_session()
|
|
56
|
+
|
|
57
|
+
def __call__(self, inputs: dict[str, Any]) -> Any:
|
|
58
|
+
"""Run inference and return every model output in ONNX Runtime order."""
|
|
59
|
+
|
|
60
|
+
return self._require_session().run(None, inputs)
|
|
61
|
+
|
|
62
|
+
def run(self, output_names: Sequence[str] | None, inputs: dict[str, Any]) -> Any:
|
|
63
|
+
"""Run inference for the requested ONNX output names."""
|
|
64
|
+
|
|
65
|
+
return self._require_session().run(output_names, inputs)
|
|
66
|
+
|
|
67
|
+
def get_inputs(self) -> Any:
|
|
68
|
+
"""Return ONNX Runtime input metadata."""
|
|
69
|
+
|
|
70
|
+
return self._require_session().get_inputs()
|
|
71
|
+
|
|
72
|
+
def get_outputs(self) -> Any:
|
|
73
|
+
"""Return ONNX Runtime output metadata."""
|
|
74
|
+
|
|
75
|
+
return self._require_session().get_outputs()
|
|
76
|
+
|
|
77
|
+
def get_dtype(self) -> str:
|
|
78
|
+
"""Return the first ONNX input element type."""
|
|
79
|
+
|
|
80
|
+
return str(self.get_inputs()[0].type)
|
|
81
|
+
|
|
82
|
+
def dispose(self) -> None:
|
|
83
|
+
"""Release the session reference held by this backend."""
|
|
84
|
+
|
|
85
|
+
self.session = None
|
|
86
|
+
|
|
87
|
+
def _require_session(self) -> Any:
|
|
88
|
+
if self.session is None:
|
|
89
|
+
raise RuntimeError("ONNX backend is not initialized; call create() first.")
|
|
90
|
+
return self.session
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Shared pytest options and fixtures for the mblt packages.
|
|
2
|
+
|
|
3
|
+
Lives here rather than as a copy in each package's tests/ for the same reason
|
|
4
|
+
npu_backend does: four copies of the NPU option parsing would drift. Each
|
|
5
|
+
package's tests/conftest.py is a star-import of this module, which is what puts
|
|
6
|
+
pytest_addoption in the root conftest namespace where pytest looks for it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import warnings
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, List, Optional
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
_WARNED_UNUSED_PREFIXES: set[str] = set()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_target_cores(value: Optional[str]) -> Optional[List[str]]:
|
|
19
|
+
if value is None:
|
|
20
|
+
return None
|
|
21
|
+
text = value.strip()
|
|
22
|
+
if not text:
|
|
23
|
+
return None
|
|
24
|
+
return [item.strip() for item in text.split(";") if item.strip()]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_target_clusters(value: Optional[str]) -> Optional[List[int]]:
|
|
28
|
+
if value is None:
|
|
29
|
+
return None
|
|
30
|
+
text = value.strip()
|
|
31
|
+
if not text:
|
|
32
|
+
return None
|
|
33
|
+
clusters: list[int] = []
|
|
34
|
+
for item in text.split(";"):
|
|
35
|
+
item = item.strip()
|
|
36
|
+
if not item:
|
|
37
|
+
continue
|
|
38
|
+
clusters.append(int(item))
|
|
39
|
+
return clusters
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _collect_npu_kwargs(
|
|
43
|
+
config: pytest.Config, prefix: str
|
|
44
|
+
) -> tuple[dict[str, Any], bool]:
|
|
45
|
+
opt_prefix = f"--{prefix}-" if prefix else "--"
|
|
46
|
+
mxq_path = config.getoption(f"{opt_prefix}mxq-path")
|
|
47
|
+
dev_no = config.getoption(f"{opt_prefix}dev-no")
|
|
48
|
+
core_mode = config.getoption(f"{opt_prefix}core-mode")
|
|
49
|
+
target_cores_raw = config.getoption(f"{opt_prefix}target-cores")
|
|
50
|
+
target_cores = _parse_target_cores(target_cores_raw)
|
|
51
|
+
target_clusters_raw = config.getoption(f"{opt_prefix}target-clusters")
|
|
52
|
+
target_clusters = _parse_target_clusters(target_clusters_raw)
|
|
53
|
+
|
|
54
|
+
kwargs: dict[str, Any] = {}
|
|
55
|
+
provided = False
|
|
56
|
+
|
|
57
|
+
if mxq_path:
|
|
58
|
+
kwargs[f"{prefix + '_' if prefix else ''}mxq_path"] = mxq_path
|
|
59
|
+
provided = True
|
|
60
|
+
if dev_no is not None:
|
|
61
|
+
kwargs[f"{prefix + '_' if prefix else ''}dev_no"] = dev_no
|
|
62
|
+
provided = True
|
|
63
|
+
if core_mode:
|
|
64
|
+
kwargs[f"{prefix + '_' if prefix else ''}core_mode"] = core_mode
|
|
65
|
+
provided = True
|
|
66
|
+
if target_cores is not None:
|
|
67
|
+
kwargs[f"{prefix + '_' if prefix else ''}target_cores"] = target_cores
|
|
68
|
+
provided = True
|
|
69
|
+
if target_clusters is not None:
|
|
70
|
+
kwargs[f"{prefix + '_' if prefix else ''}target_clusters"] = target_clusters
|
|
71
|
+
provided = True
|
|
72
|
+
|
|
73
|
+
return kwargs, provided
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class NpuParams:
|
|
78
|
+
base: dict[str, Any]
|
|
79
|
+
encoder: dict[str, Any]
|
|
80
|
+
decoder: dict[str, Any]
|
|
81
|
+
text: dict[str, Any]
|
|
82
|
+
vision: dict[str, Any]
|
|
83
|
+
_provided: dict[str, bool]
|
|
84
|
+
|
|
85
|
+
def warn_unused(self, used_prefixes: set[str]) -> None:
|
|
86
|
+
for prefix, provided in self._provided.items():
|
|
87
|
+
if (
|
|
88
|
+
provided
|
|
89
|
+
and prefix not in used_prefixes
|
|
90
|
+
and prefix not in _WARNED_UNUSED_PREFIXES
|
|
91
|
+
):
|
|
92
|
+
_WARNED_UNUSED_PREFIXES.add(prefix)
|
|
93
|
+
warnings.warn(
|
|
94
|
+
f"Provided {prefix} NPU backend options will be ignored for this model.",
|
|
95
|
+
UserWarning,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def pytest_addoption(parser):
|
|
100
|
+
parser.addoption(
|
|
101
|
+
"--mxq-path",
|
|
102
|
+
action="store",
|
|
103
|
+
default=None,
|
|
104
|
+
help="Override default mxq_path for pipeline loading.",
|
|
105
|
+
)
|
|
106
|
+
parser.addoption(
|
|
107
|
+
"--dev-no",
|
|
108
|
+
action="store",
|
|
109
|
+
default=None,
|
|
110
|
+
type=int,
|
|
111
|
+
help="NPU device number.",
|
|
112
|
+
)
|
|
113
|
+
parser.addoption(
|
|
114
|
+
"--core-mode",
|
|
115
|
+
action="store",
|
|
116
|
+
default=None,
|
|
117
|
+
help="NPU core mode (single, multi, global4, global8).",
|
|
118
|
+
)
|
|
119
|
+
parser.addoption(
|
|
120
|
+
"--target-cores",
|
|
121
|
+
action="store",
|
|
122
|
+
default=None,
|
|
123
|
+
help='Target cores (e.g., "0:0;0:1;0:2;0:3").',
|
|
124
|
+
)
|
|
125
|
+
parser.addoption(
|
|
126
|
+
"--target-clusters",
|
|
127
|
+
action="store",
|
|
128
|
+
default=None,
|
|
129
|
+
help='Target clusters (e.g., "0;1").',
|
|
130
|
+
)
|
|
131
|
+
for prefix in ("encoder", "decoder", "vision", "text"):
|
|
132
|
+
parser.addoption(
|
|
133
|
+
f"--{prefix}-mxq-path",
|
|
134
|
+
action="store",
|
|
135
|
+
default=None,
|
|
136
|
+
help=f"Override {prefix} mxq_path.",
|
|
137
|
+
)
|
|
138
|
+
parser.addoption(
|
|
139
|
+
f"--{prefix}-dev-no",
|
|
140
|
+
action="store",
|
|
141
|
+
default=None,
|
|
142
|
+
type=int,
|
|
143
|
+
help=f"{prefix} NPU device number.",
|
|
144
|
+
)
|
|
145
|
+
parser.addoption(
|
|
146
|
+
f"--{prefix}-core-mode",
|
|
147
|
+
action="store",
|
|
148
|
+
default=None,
|
|
149
|
+
help=f"{prefix} NPU core mode (single, multi, global4, global8).",
|
|
150
|
+
)
|
|
151
|
+
parser.addoption(
|
|
152
|
+
f"--{prefix}-target-cores",
|
|
153
|
+
action="store",
|
|
154
|
+
default=None,
|
|
155
|
+
help=f'{prefix} target cores (e.g., "0:0;0:1;0:2;0:3").',
|
|
156
|
+
)
|
|
157
|
+
parser.addoption(
|
|
158
|
+
f"--{prefix}-target-clusters",
|
|
159
|
+
action="store",
|
|
160
|
+
default=None,
|
|
161
|
+
help=f'{prefix} target clusters (e.g., "0;1").',
|
|
162
|
+
)
|
|
163
|
+
parser.addoption(
|
|
164
|
+
"--revision",
|
|
165
|
+
action="store",
|
|
166
|
+
default=None,
|
|
167
|
+
help="Override model revision (e.g., W8).",
|
|
168
|
+
)
|
|
169
|
+
parser.addoption(
|
|
170
|
+
"--embedding-weight",
|
|
171
|
+
action="store",
|
|
172
|
+
default=None,
|
|
173
|
+
help="Path to custom embedding weights.",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.fixture(scope="module")
|
|
178
|
+
def mxq_path(request):
|
|
179
|
+
return request.config.getoption("--mxq-path")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.fixture(scope="module")
|
|
183
|
+
def revision(request):
|
|
184
|
+
return request.config.getoption("--revision")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.fixture(scope="module")
|
|
188
|
+
def embedding_weight(request):
|
|
189
|
+
return request.config.getoption("--embedding-weight")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@pytest.fixture(scope="module")
|
|
193
|
+
def npu_params(request, embedding_weight):
|
|
194
|
+
config = request.config
|
|
195
|
+
base_kwargs, base_provided = _collect_npu_kwargs(config, "")
|
|
196
|
+
if embedding_weight:
|
|
197
|
+
base_kwargs["embedding_weight"] = embedding_weight
|
|
198
|
+
base_provided = True
|
|
199
|
+
|
|
200
|
+
encoder_kwargs, encoder_provided = _collect_npu_kwargs(config, "encoder")
|
|
201
|
+
decoder_kwargs, decoder_provided = _collect_npu_kwargs(config, "decoder")
|
|
202
|
+
vision_kwargs, vision_provided = _collect_npu_kwargs(config, "vision")
|
|
203
|
+
text_kwargs, text_provided = _collect_npu_kwargs(config, "text")
|
|
204
|
+
|
|
205
|
+
return NpuParams(
|
|
206
|
+
base=base_kwargs,
|
|
207
|
+
encoder=encoder_kwargs,
|
|
208
|
+
decoder=decoder_kwargs,
|
|
209
|
+
text=text_kwargs,
|
|
210
|
+
vision=vision_kwargs,
|
|
211
|
+
_provided={
|
|
212
|
+
"base": base_provided,
|
|
213
|
+
"encoder": encoder_provided,
|
|
214
|
+
"decoder": decoder_provided,
|
|
215
|
+
"vision": vision_provided,
|
|
216
|
+
"text": text_provided,
|
|
217
|
+
},
|
|
218
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mblt-npu-python
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Shared NPU access for the Mobilint Python packages
|
|
5
|
+
Author: Mobilint
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Project-URL: Home, https://www.mobilint.com/
|
|
8
|
+
Project-URL: Repository, https://github.com/mobilint/mblt-npu-python
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: <3.13,>=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: mobilint-qb-runtime>=1.0.0
|
|
22
|
+
Requires-Dist: huggingface-hub
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Provides-Extra: onnxruntime
|
|
26
|
+
Requires-Dist: onnxruntime; extra == "onnxruntime"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# Mobilint NPU Python
|
|
30
|
+
|
|
31
|
+
Shared runtime support for applications that run MXQ models on Mobilint NPUs or ONNX models through ONNX Runtime.
|
|
32
|
+
`mblt-npu-python` provides the common backend, device-selection rules, Hugging Face
|
|
33
|
+
artifact resolution, and model-detail logging used by Mobilint Python packages. It
|
|
34
|
+
is a library dependency, rather than an end-user model catalog.
|
|
35
|
+
|
|
36
|
+
Version `0.0.0` is the initial standalone release.
|
|
37
|
+
|
|
38
|
+
`logging` ships here rather than with its only caller because the two are mutually
|
|
39
|
+
dependent — `npu_backend` imports `log_model_details`, and `log_model_details` reads
|
|
40
|
+
a `MobilintNPUBackend`'s fields.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install mblt-npu-python
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This package requires a supported Linux environment with
|
|
49
|
+
[`mobilint-qb-runtime`](https://pypi.org/project/mobilint-qb-runtime/) available
|
|
50
|
+
and Python 3.10 through 3.12.
|
|
51
|
+
|
|
52
|
+
## Public API
|
|
53
|
+
|
|
54
|
+
Import the backend from `mblt_npu`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from mblt_npu import MobilintNPUBackend
|
|
58
|
+
|
|
59
|
+
backend = MobilintNPUBackend(
|
|
60
|
+
mxq_path="model.mxq",
|
|
61
|
+
core_mode="single",
|
|
62
|
+
)
|
|
63
|
+
backend.create()
|
|
64
|
+
try:
|
|
65
|
+
backend.launch()
|
|
66
|
+
outputs = backend.mxq_model.infer([input_tensor])
|
|
67
|
+
finally:
|
|
68
|
+
backend.dispose()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`MobilintNPUBackend` selects the appropriate implementation from
|
|
72
|
+
`target_device` (default: `"aries-rb"`). `"aries-rb"` selects
|
|
73
|
+
`MobilintAriesBackend`; `"regulus-ra"` and `"regulus-rb"` select
|
|
74
|
+
`MobilintRegulusBackend`. The former generic values `"aries"` and `"regulus"`
|
|
75
|
+
remain accepted when loading older configurations.
|
|
76
|
+
`backend_class_for()` and `BACKEND_CLASSES` are available for integrations that
|
|
77
|
+
need to inspect the supported targets.
|
|
78
|
+
|
|
79
|
+
For ONNX inference, install the optional runtime extra and use `ONNXBackend`:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install "mblt-npu-python[onnxruntime]"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from mblt_npu import ONNXBackend
|
|
87
|
+
|
|
88
|
+
backend = ONNXBackend("model.onnx")
|
|
89
|
+
backend.create()
|
|
90
|
+
outputs = backend({"images": input_array})
|
|
91
|
+
backend.dispose()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`ONNXBackend` imports `onnxruntime` only when it creates a session.
|
|
95
|
+
|
|
96
|
+
Most users should access the backend through a model package such as
|
|
97
|
+
[`mblt-vision-python`](https://github.com/mobilint/mblt-vision-python), which owns
|
|
98
|
+
model configuration, preprocessing, and postprocessing.
|
|
99
|
+
|
|
100
|
+
## Testing helpers
|
|
101
|
+
|
|
102
|
+
The optional `test` extra provides a shared pytest plugin with NPU options and the
|
|
103
|
+
`npu_params` fixture used by Mobilint package test suites:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install "mblt-npu-python[test]"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Import `mblt_npu.pytest_plugin` from a repository's root `tests/conftest.py` to
|
|
110
|
+
register its options. The plugin is intentionally not auto-registered, so projects
|
|
111
|
+
control when those command-line options are exposed.
|
|
112
|
+
|
|
113
|
+
## Support and issues
|
|
114
|
+
|
|
115
|
+
For installation, runtime, or integration support, visit the
|
|
116
|
+
[Mobilint forum](https://discuss.mobilint.com/). Report reproducible package issues in the
|
|
117
|
+
[mblt-npu-python issue tracker](https://github.com/mobilint/mblt-npu-python/issues).
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
Distributed under the [BSD 3-Clause License](LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
mblt_npu/__init__.py,sha256=u19Xfg_XvdmOLGQnTTwR_8g1l2-H_K_Q6vpjHI9w0Xw,1018
|
|
2
|
+
mblt_npu/logging.py,sha256=vmNHi7IgX8PUD58EwyJLtw-g1iJJMt6_Al4xGyLrV0A,2005
|
|
3
|
+
mblt_npu/npu_backend.py,sha256=cKJBNOzUodo_xkIxZTQWLXUy2ZSHGo7YgfadBRXuxuc,25257
|
|
4
|
+
mblt_npu/onnx_backend.py,sha256=kVkGGWxA-QobPADHLxN1wM53XKtyBPUSHxuPboDK2AY,2885
|
|
5
|
+
mblt_npu/pytest_plugin.py,sha256=-qHifafGPmhuh1HDUCGU9i4Kdm5R9c1QoascrvwuUlc,6603
|
|
6
|
+
mblt_npu_python-0.0.0.dist-info/licenses/LICENSE,sha256=uoT2s1wxohq-vNpQBIqumGsAky_kRTLY_LNdSrLjXZM,1501
|
|
7
|
+
mblt_npu_python-0.0.0.dist-info/METADATA,sha256=cIdGQ4_WCrtW8jvTw7vkboJPb_B1i_MA49UJ_lvWwCY,4000
|
|
8
|
+
mblt_npu_python-0.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
mblt_npu_python-0.0.0.dist-info/top_level.txt,sha256=Bp6_Sz4yMUXGejQXslSAfoDx8RjPw_WPBkOOFQSLaXk,9
|
|
10
|
+
mblt_npu_python-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Mobilint, Inc.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mblt_npu
|