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 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
+ )
@@ -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
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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