mblt-npu-python 0.0.1__tar.gz → 0.1.0__tar.gz

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.
Files changed (20) hide show
  1. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/PKG-INFO +7 -5
  2. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/README.md +5 -3
  3. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu/__init__.py +1 -1
  4. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu/npu_backend.py +39 -9
  5. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu/npu_target.py +109 -7
  6. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/PKG-INFO +7 -5
  7. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/requires.txt +1 -1
  8. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/pyproject.toml +1 -1
  9. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/tests/test_npu_backend_multi_slot.py +3 -2
  10. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/tests/test_target_device.py +184 -1
  11. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/LICENSE +0 -0
  12. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu/core_mode.py +0 -0
  13. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu/logging.py +0 -0
  14. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu/onnx_backend.py +0 -0
  15. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu/pytest_plugin.py +0 -0
  16. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/SOURCES.txt +0 -0
  17. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/dependency_links.txt +0 -0
  18. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/top_level.txt +0 -0
  19. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/setup.cfg +0 -0
  20. {mblt_npu_python-0.0.1 → mblt_npu_python-0.1.0}/tests/test_onnx_backend.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mblt-npu-python
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: Shared NPU access for the Mobilint Python packages
5
5
  Author: Mobilint
6
6
  License: BSD-3-Clause
@@ -18,7 +18,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
18
  Requires-Python: <3.13,>=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: mobilint-qb-runtime>=1.0.0
21
+ Requires-Dist: mobilint-qb-runtime>=1.4.0
22
22
  Requires-Dist: huggingface-hub
23
23
  Provides-Extra: test
24
24
  Requires-Dist: pytest; extra == "test"
@@ -84,9 +84,11 @@ finally:
84
84
 
85
85
  `MobilintNPUBackend` selects the appropriate implementation from
86
86
  `target_device` (default: `"aries-rb"`). `"aries-rb"` selects
87
- `MobilintAriesBackend`; `"regulus-ra"` and `"regulus-rb"` select
88
- `MobilintRegulusBackend`. The former generic values `"aries"` and `"regulus"`
89
- remain accepted when loading older configurations.
87
+ `MobilintAriesBackend`; `"regulus-ra"`, `"regulus-rb"`, `"regulus-ra-usb"`,
88
+ and `"regulus-rb-usb"` select `MobilintRegulusBackend`. The former generic
89
+ values `"aries"` and `"regulus"` remain accepted when loading older
90
+ configurations. The board name is forwarded to `qbruntime.Accelerator` so the
91
+ runtime opens the matching device (requires `mobilint-qb-runtime>=1.4.0`).
90
92
  `backend_class_for()` and `BACKEND_CLASSES` are available for integrations that
91
93
  need to inspect the supported targets.
92
94
 
@@ -56,9 +56,11 @@ finally:
56
56
 
57
57
  `MobilintNPUBackend` selects the appropriate implementation from
58
58
  `target_device` (default: `"aries-rb"`). `"aries-rb"` selects
59
- `MobilintAriesBackend`; `"regulus-ra"` and `"regulus-rb"` select
60
- `MobilintRegulusBackend`. The former generic values `"aries"` and `"regulus"`
61
- remain accepted when loading older configurations.
59
+ `MobilintAriesBackend`; `"regulus-ra"`, `"regulus-rb"`, `"regulus-ra-usb"`,
60
+ and `"regulus-rb-usb"` select `MobilintRegulusBackend`. The former generic
61
+ values `"aries"` and `"regulus"` remain accepted when loading older
62
+ configurations. The board name is forwarded to `qbruntime.Accelerator` so the
63
+ runtime opens the matching device (requires `mobilint-qb-runtime>=1.4.0`).
62
64
  `backend_class_for()` and `BACKEND_CLASSES` are available for integrations that
63
65
  need to inspect the supported targets.
64
66
 
@@ -22,7 +22,7 @@ from .npu_backend import (
22
22
  from .logging import log_model_details
23
23
  from .onnx_backend import ONNXBackend
24
24
 
25
- __version__ = "0.0.1"
25
+ __version__ = "0.1.0"
26
26
 
27
27
  __all__ = [
28
28
  "BACKEND_CLASSES",
@@ -31,7 +31,7 @@ import math
31
31
  import os
32
32
  import re
33
33
  import sys
34
- from typing import Any, Dict, List, Literal, Optional, Union
34
+ from typing import Any, Dict, List, Literal, Optional, Sequence, Union
35
35
 
36
36
  from huggingface_hub import HfApi, hf_hub_download
37
37
  from huggingface_hub.errors import EntryNotFoundError
@@ -74,7 +74,13 @@ def normalize_target_device(target_device: str) -> str:
74
74
  normalized = _TARGET_DEVICE_ALIASES.get(
75
75
  target_device.lower(), target_device.lower()
76
76
  )
77
- if normalized not in {"aries-rb", "regulus-ra", "regulus-rb"}:
77
+ if normalized not in {
78
+ "aries-rb",
79
+ "regulus-ra",
80
+ "regulus-rb",
81
+ "regulus-ra-usb",
82
+ "regulus-rb-usb",
83
+ }:
78
84
  raise ValueError(f"unknown target_device {target_device!r}")
79
85
  return normalized
80
86
 
@@ -194,7 +200,13 @@ class MobilintNPUBackend:
194
200
  num_of_cores_in_cluster = 4
195
201
 
196
202
  default_target_device = DEFAULT_TARGET_DEVICE
197
- supported_target_devices = ("aries-rb", "regulus-ra", "regulus-rb")
203
+ supported_target_devices = (
204
+ "aries-rb",
205
+ "regulus-ra",
206
+ "regulus-rb",
207
+ "regulus-ra-usb",
208
+ "regulus-rb-usb",
209
+ )
198
210
 
199
211
  def __new__(cls, *args: Any, **kwargs: Any):
200
212
  """Select a board-specific backend while retaining the legacy constructor."""
@@ -211,7 +223,7 @@ class MobilintNPUBackend:
211
223
  dev_no: Optional[Union[int, List[int]]] = None,
212
224
  core_mode: CoreMode = "single",
213
225
  target_cores: Optional[List[Union[str, "CoreId"]]] = None,
214
- target_clusters: Optional[List[Union[int, str, "Cluster"]]] = None,
226
+ target_clusters: Optional[Sequence[Union[int, str, "Cluster"]]] = None,
215
227
  revision: Optional[str] = None,
216
228
  commit_hash: Optional[str] = None,
217
229
  target_device: Optional[str] = None,
@@ -300,9 +312,14 @@ class MobilintNPUBackend:
300
312
  spec_kwargs["target_cores"] = list(target_cores)
301
313
  if target_clusters is not None:
302
314
  spec_kwargs["target_clusters"] = list(target_clusters)
315
+ # Config-layer normalization reads ``target_device`` directly from
316
+ # ``spec_kwargs``. Setter-chain normalization runs later without
317
+ # target_device in scope, so seed it on the pending here.
318
+ spec_kwargs["target_device"] = self.target_device
303
319
  initial_spec = NPUTargetSpec.from_kwargs(spec_kwargs)
304
320
  self._pending: NPUTargetSpecPending = NPUTargetSpecPending(
305
- baseline=initial_spec
321
+ baseline=initial_spec,
322
+ target_device=self.target_device,
306
323
  )
307
324
  self._finalized: Optional[NPUTargetSpec] = initial_spec
308
325
 
@@ -337,7 +354,11 @@ class MobilintNPUBackend:
337
354
  self._finalized = self._pending.finalize()
338
355
  # Close the current override epoch: the next setter chain
339
356
  # accumulates on a fresh baseline with all intent flags cleared.
340
- self._pending = NPUTargetSpecPending.from_baseline(self._finalized)
357
+ # Carry ``target_device`` forward so a later setter chain that
358
+ # re-expands ``dev_no`` sugar still sees the backend's board.
359
+ self._pending = NPUTargetSpecPending.from_baseline(
360
+ self._finalized, target_device=self.target_device
361
+ )
341
362
  return self._finalized
342
363
 
343
364
  # ---- Target-topology accessors ------------------------------------------
@@ -943,7 +964,9 @@ class MobilintNPUBackend:
943
964
  if not unique_devs:
944
965
  unique_devs = [self._fallback_dev()]
945
966
 
946
- self.accs = {int(d): Accelerator(int(d)) for d in unique_devs}
967
+ self.accs = {
968
+ int(d): Accelerator(self.target_device, int(d)) for d in unique_devs
969
+ }
947
970
  self.mxq_models = []
948
971
  self.model_dev_no = []
949
972
  self.n_models = 0
@@ -1350,7 +1373,7 @@ class MobilintNPUBackend:
1350
1373
  return result
1351
1374
 
1352
1375
  @target_clusters.setter
1353
- def target_clusters(self, values: List[Union[int, str, "Cluster"]]) -> None:
1376
+ def target_clusters(self, values: Sequence[Union[int, str, "Cluster"]]) -> None:
1354
1377
  """Record a raw ``target_clusters`` override on the pending accumulator.
1355
1378
 
1356
1379
  Normalization (legacy migration, grain fold/unfold, device-set
@@ -1489,7 +1512,12 @@ class MobilintRegulusBackend(MobilintNPUBackend):
1489
1512
  """Regulus backend: validates its one-core, single/auto-only topology."""
1490
1513
 
1491
1514
  default_target_device = "regulus-ra"
1492
- supported_target_devices = ("regulus-ra", "regulus-rb")
1515
+ supported_target_devices = (
1516
+ "regulus-ra",
1517
+ "regulus-rb",
1518
+ "regulus-ra-usb",
1519
+ "regulus-rb-usb",
1520
+ )
1493
1521
  num_of_clusters = 1
1494
1522
  num_of_cores_in_cluster = 1
1495
1523
 
@@ -1576,6 +1604,8 @@ BACKEND_CLASSES = {
1576
1604
  "aries-rb": MobilintAriesBackend,
1577
1605
  "regulus-ra": MobilintRegulusBackend,
1578
1606
  "regulus-rb": MobilintRegulusBackend,
1607
+ "regulus-ra-usb": MobilintRegulusBackend,
1608
+ "regulus-rb-usb": MobilintRegulusBackend,
1579
1609
  }
1580
1610
 
1581
1611
 
@@ -52,11 +52,32 @@ from .core_mode import CoreMode, normalize_core_mode
52
52
  # Default device index for ``dev_no`` when the caller does not pin one.
53
53
  _DEFAULT_DEV_NO: int = 0
54
54
 
55
+
55
56
  # Sentinel used to distinguish "field not overridden this session" from
56
57
  # "field explicitly set to None/empty". Kept module-private and reused by
57
58
  # both :class:`NPUTargetSpec` (single-call ``_with`` shim) and
58
59
  # :class:`NPUTargetSpecPending` (accumulator).
59
- _UNSET: Any = object()
60
+ #
61
+ # Identity, not equality, is what every ``raw_* is not _UNSET`` check relies
62
+ # on. A bare ``object()`` does not survive ``copy.deepcopy`` as the same
63
+ # instance, and upstream ``transformers.PretrainedConfig.to_dict()`` runs
64
+ # ``copy.deepcopy(self.__dict__)`` on every config it serializes -- including
65
+ # nested sub-configs whose ``npu_backend`` still holds a pending spec with
66
+ # unresolved ``_UNSET`` slots. That deep-copied sentinel then fails every
67
+ # ``is not _UNSET`` check, so callers see a bogus "overridden" field. Make the
68
+ # sentinel copy/deepcopy to itself so its identity survives that path.
69
+ class _UnsetType:
70
+ def __repr__(self) -> str:
71
+ return "_UNSET"
72
+
73
+ def __copy__(self) -> "_UnsetType":
74
+ return self
75
+
76
+ def __deepcopy__(self, memo: Dict[int, Any]) -> "_UnsetType":
77
+ return self
78
+
79
+
80
+ _UNSET: Any = _UnsetType()
60
81
 
61
82
 
62
83
  cluster_map: Dict[int, "Cluster"] = {
@@ -110,6 +131,33 @@ _VALID_CLUSTER_INDICES: frozenset = frozenset(cluster_map.keys())
110
131
  _VALID_CORE_INDICES: frozenset = frozenset(core_map.keys())
111
132
 
112
133
 
134
+ def _topology_for_target(target_device: Optional[str]) -> tuple[int, int]:
135
+ """Return ``(num_clusters, num_cores_per_cluster)`` for a board identifier.
136
+
137
+ Consumed by :func:`_resolve_targets` when expanding ``dev_no`` sugar into
138
+ a default target-core / target-cluster list: without a board-appropriate
139
+ topology, a Regulus config that names only ``target_device`` would be
140
+ populated with Aries's 2×4 grid and then rejected by
141
+ :class:`MobilintRegulusBackend` for referencing cores its single-cluster
142
+ hardware does not have.
143
+
144
+ Args:
145
+ target_device: The board identifier from the caller's config
146
+ (``"aries-rb"``, ``"regulus-rb-usb"``, legacy ``"aries"`` /
147
+ ``"regulus"``, ...), or ``None`` when the caller has not
148
+ declared one. Case-insensitive.
149
+
150
+ Returns:
151
+ ``(2, 4)`` for Aries (and the historical unspecified default so
152
+ callers with legacy configs keep the same sugar expansion); ``(1, 1)``
153
+ for every Regulus variant (RA / RB / RA-USB / RB-USB, the ``regulus``
154
+ family alias, and any future ``regulus-*`` addition).
155
+ """
156
+ if target_device and target_device.lower().startswith("regulus"):
157
+ return (1, 1)
158
+ return (2, 4)
159
+
160
+
113
161
  def _check_cluster_core_indices(c_val: int, k_val: int, entry: Any) -> None:
114
162
  """Raise ``ValueError`` when ``c_val`` / ``k_val`` fall outside the Aries2 topology."""
115
163
  if (
@@ -453,6 +501,16 @@ class NPUTargetSpec:
453
501
  _pending: Optional["NPUTargetSpecPending"] = field(
454
502
  default=None, compare=False, hash=False, repr=False
455
503
  )
504
+ # Board identifier carried alongside the canonical fields so a subsequent
505
+ # :meth:`_with` (or a fresh :class:`NPUTargetSpecPending` derived from
506
+ # this spec) still knows which topology to use when ``dev_no`` sugar has
507
+ # to re-expand. Excluded from equality/hash/repr because it is not part
508
+ # of the canonical ``(dev_no, core_mode, cores, clusters)`` identity —
509
+ # two Aries specs with identical grain must still compare equal even if
510
+ # only one of them recorded ``target_device``.
511
+ target_device: Optional[str] = field(
512
+ default=None, compare=False, hash=False, repr=False
513
+ )
456
514
 
457
515
  @property
458
516
  def _dev_no_overridden(self) -> bool:
@@ -518,6 +576,7 @@ class NPUTargetSpec:
518
576
  dev_no_key = f"{prefix}dev_no"
519
577
  cores_key = f"{prefix}target_cores"
520
578
  clusters_key = f"{prefix}target_clusters"
579
+ target_device_key = f"{prefix}target_device"
521
580
 
522
581
  core_mode = normalize_core_mode(kwargs.get(core_mode_key, "single"))
523
582
  dev_no = _dedup_dev_no(kwargs.get(dev_no_key, _DEFAULT_DEV_NO))
@@ -525,6 +584,7 @@ class NPUTargetSpec:
525
584
  dev_list = _normalize_dev_list(dev_no)
526
585
  fallback_dev = dev_list[0]
527
586
  dev_no_given = dev_no_key in kwargs
587
+ target_device = kwargs.get(target_device_key)
528
588
 
529
589
  raw_cores = kwargs.get(cores_key)
530
590
  raw_clusters = kwargs.get(clusters_key)
@@ -537,6 +597,7 @@ class NPUTargetSpec:
537
597
  dev_no_given=dev_no_given,
538
598
  raw_cores=list(raw_cores) if raw_cores else [],
539
599
  raw_clusters=list(raw_clusters) if raw_clusters else [],
600
+ target_device=target_device,
540
601
  )
541
602
 
542
603
  # Mutate the caller's ``kwargs`` dict in place for downstream code
@@ -566,6 +627,7 @@ class NPUTargetSpec:
566
627
  core_mode=core_mode,
567
628
  cores=tuple(cores),
568
629
  clusters=tuple(clusters),
630
+ target_device=target_device,
569
631
  )
570
632
 
571
633
  def _with(
@@ -606,7 +668,10 @@ class NPUTargetSpec:
606
668
  :meth:`NPUTargetSpecPending.finalize`.
607
669
  """
608
670
  if self._pending is None:
609
- base_pending = NPUTargetSpecPending(baseline=replace(self, _pending=None))
671
+ base_pending = NPUTargetSpecPending(
672
+ baseline=replace(self, _pending=None),
673
+ target_device=self.target_device,
674
+ )
610
675
  else:
611
676
  base_pending = self._pending
612
677
  new_pending = base_pending._with(
@@ -663,6 +728,7 @@ def _resolve_targets(
663
728
  dev_no_given: bool,
664
729
  raw_cores: List[Any],
665
730
  raw_clusters: List[Any],
731
+ target_device: Optional[str] = None,
666
732
  ) -> tuple[List[str], List[str]]:
667
733
  """Canonicalize a ``(dev_no, core_mode, cores, clusters)`` payload.
668
734
 
@@ -700,11 +766,24 @@ def _resolve_targets(
700
766
  )
701
767
 
702
768
  if not cores and not clusters:
703
- # ``dev_no`` sugar expansion when both target lists are absent.
769
+ # ``dev_no`` sugar expansion when both target lists are absent. The
770
+ # topology is board-dependent: Aries exposes 2 clusters × 4 cores per
771
+ # device, Regulus exposes 1 cluster × 1 core. Without threading
772
+ # ``target_device`` here, a Regulus config that only names
773
+ # ``target_device`` receives Aries's 8-core grid and later fails
774
+ # :class:`MobilintRegulusBackend`'s topology check.
775
+ num_clusters, num_cores = _topology_for_target(target_device)
704
776
  if core_mode == "single":
705
- cores = [f"{d}:{c}:{k}" for d in dev_list for c in (0, 1) for k in range(4)]
777
+ cores = [
778
+ f"{d}:{c}:{k}"
779
+ for d in dev_list
780
+ for c in range(num_clusters)
781
+ for k in range(num_cores)
782
+ ]
706
783
  else:
707
- clusters = [f"{d}:{c}" for d in dev_list for c in (0, 1)]
784
+ clusters = [
785
+ f"{d}:{c}" for d in dev_list for c in range(num_clusters)
786
+ ]
708
787
  else:
709
788
  # Grain unification per core_mode.
710
789
  if core_mode == "single":
@@ -776,6 +855,13 @@ class NPUTargetSpecPending:
776
855
  strings, or canonical ``"d:c:k"`` strings), or :data:`_UNSET`.
777
856
  raw_clusters: The caller's raw ``target_clusters`` override, or
778
857
  :data:`_UNSET`.
858
+ target_device: Board identifier carried alongside the override
859
+ history so :meth:`finalize` can pick the right ``dev_no`` sugar
860
+ topology (Aries 2×4 vs Regulus 1×1). Attached by the backend at
861
+ init time (and preserved across chained :meth:`_with` /
862
+ :meth:`from_baseline` calls) because the spec-level normalization
863
+ layer sees target_device only at the config-load entry point;
864
+ per-field setter chains would otherwise lose it.
779
865
  """
780
866
 
781
867
  baseline: NPUTargetSpec
@@ -783,9 +869,15 @@ class NPUTargetSpecPending:
783
869
  raw_core_mode: Any = _UNSET
784
870
  raw_cores: Any = _UNSET
785
871
  raw_clusters: Any = _UNSET
872
+ target_device: Optional[str] = None
786
873
 
787
874
  @classmethod
788
- def from_baseline(cls, spec: NPUTargetSpec) -> "NPUTargetSpecPending":
875
+ def from_baseline(
876
+ cls,
877
+ spec: NPUTargetSpec,
878
+ *,
879
+ target_device: Optional[str] = None,
880
+ ) -> "NPUTargetSpecPending":
789
881
  """Return a fresh pending baseline for the next override epoch.
790
882
 
791
883
  :class:`MobilintNPUBackend` calls this immediately after materializing
@@ -801,13 +893,21 @@ class NPUTargetSpecPending:
801
893
  spec: Canonical :class:`NPUTargetSpec` to seed the fresh pending's
802
894
  baseline with. Typically the finalized result of the previous
803
895
  override epoch.
896
+ target_device: Board identifier attached to the fresh pending so
897
+ :meth:`finalize` picks the right ``dev_no`` sugar topology.
898
+ Defaults to ``spec.target_device`` so callers that omit it
899
+ (or specs constructed outside the backend) still keep their
900
+ board; the backend's explicit override wins when provided.
804
901
 
805
902
  Returns:
806
903
  A new :class:`NPUTargetSpecPending` whose baseline is ``spec``
807
904
  (with any prior ``_pending`` history stripped) and whose intent
808
905
  slots are all :data:`_UNSET`.
809
906
  """
810
- return cls(baseline=replace(spec, _pending=None))
907
+ return cls(
908
+ baseline=replace(spec, _pending=None),
909
+ target_device=target_device if target_device is not None else spec.target_device,
910
+ )
811
911
 
812
912
  def _with(
813
913
  self,
@@ -941,6 +1041,7 @@ class NPUTargetSpecPending:
941
1041
  dev_no_given=dev_no_overridden,
942
1042
  raw_cores=raw_cores,
943
1043
  raw_clusters=raw_clusters,
1044
+ target_device=self.target_device,
944
1045
  )
945
1046
 
946
1047
  # Resolve the effective ``dev_no`` for the returned canonical spec.
@@ -962,6 +1063,7 @@ class NPUTargetSpecPending:
962
1063
  cores=tuple(cores),
963
1064
  clusters=tuple(clusters),
964
1065
  _pending=self,
1066
+ target_device=self.target_device,
965
1067
  )
966
1068
 
967
1069
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mblt-npu-python
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: Shared NPU access for the Mobilint Python packages
5
5
  Author: Mobilint
6
6
  License: BSD-3-Clause
@@ -18,7 +18,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
18
  Requires-Python: <3.13,>=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: mobilint-qb-runtime>=1.0.0
21
+ Requires-Dist: mobilint-qb-runtime>=1.4.0
22
22
  Requires-Dist: huggingface-hub
23
23
  Provides-Extra: test
24
24
  Requires-Dist: pytest; extra == "test"
@@ -84,9 +84,11 @@ finally:
84
84
 
85
85
  `MobilintNPUBackend` selects the appropriate implementation from
86
86
  `target_device` (default: `"aries-rb"`). `"aries-rb"` selects
87
- `MobilintAriesBackend`; `"regulus-ra"` and `"regulus-rb"` select
88
- `MobilintRegulusBackend`. The former generic values `"aries"` and `"regulus"`
89
- remain accepted when loading older configurations.
87
+ `MobilintAriesBackend`; `"regulus-ra"`, `"regulus-rb"`, `"regulus-ra-usb"`,
88
+ and `"regulus-rb-usb"` select `MobilintRegulusBackend`. The former generic
89
+ values `"aries"` and `"regulus"` remain accepted when loading older
90
+ configurations. The board name is forwarded to `qbruntime.Accelerator` so the
91
+ runtime opens the matching device (requires `mobilint-qb-runtime>=1.4.0`).
90
92
  `backend_class_for()` and `BACKEND_CLASSES` are available for integrations that
91
93
  need to inspect the supported targets.
92
94
 
@@ -1,4 +1,4 @@
1
- mobilint-qb-runtime>=1.0.0
1
+ mobilint-qb-runtime>=1.4.0
2
2
  huggingface-hub
3
3
 
4
4
  [onnxruntime]
@@ -8,7 +8,7 @@ dynamic = ["version"]
8
8
  description = "Shared NPU access for the Mobilint Python packages"
9
9
  readme = "README.md"
10
10
  dependencies = [
11
- "mobilint-qb-runtime>=1.0.0",
11
+ "mobilint-qb-runtime>=1.4.0",
12
12
  "huggingface-hub",
13
13
  ]
14
14
  requires-python = ">=3.10,<3.13"
@@ -28,9 +28,10 @@ from mblt_npu.npu_backend import (
28
28
 
29
29
 
30
30
  class _FakeAccelerator:
31
- """Records the device number an accelerator was opened for."""
31
+ """Records the target device name and device number an accelerator was opened for."""
32
32
 
33
- def __init__(self, dev_no: int) -> None:
33
+ def __init__(self, target_device: str, dev_no: int) -> None:
34
+ self.target_device = target_device
34
35
  self.dev_no = int(dev_no)
35
36
 
36
37
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import copy
5
6
  from pathlib import Path
6
7
  from typing import Any, cast
7
8
 
@@ -9,6 +10,7 @@ import pytest
9
10
 
10
11
  from mblt_npu import MobilintAriesBackend, MobilintNPUBackend, MobilintRegulusBackend
11
12
  import mblt_npu.npu_backend as npu_backend
13
+ from mblt_npu.npu_target import _UNSET, NPUTargetSpec
12
14
 
13
15
 
14
16
  @pytest.mark.parametrize(
@@ -17,6 +19,8 @@ import mblt_npu.npu_backend as npu_backend
17
19
  ("aries-rb", MobilintAriesBackend),
18
20
  ("regulus-ra", MobilintRegulusBackend),
19
21
  ("regulus-rb", MobilintRegulusBackend),
22
+ ("regulus-ra-usb", MobilintRegulusBackend),
23
+ ("regulus-rb-usb", MobilintRegulusBackend),
20
24
  ],
21
25
  )
22
26
  def test_target_device_selects_the_product_backend(
@@ -31,6 +35,40 @@ def test_target_device_selects_the_product_backend(
31
35
  assert backend.to_dict()["target_device"] == target_device
32
36
 
33
37
 
38
+ def test_unset_sentinel_survives_deepcopy() -> None:
39
+ """Keep the ``_UNSET`` sentinel's identity across ``copy.deepcopy``.
40
+
41
+ ``transformers.PretrainedConfig.to_dict()`` runs ``copy.deepcopy(self.__dict__)``
42
+ on every config it serializes, including nested sub-configs whose
43
+ ``npu_backend`` still holds unresolved ``_UNSET`` slots. A bare ``object()``
44
+ sentinel would be reconstructed as a distinct instance there, so every
45
+ ``is not _UNSET`` override check on the copy would wrongly report the
46
+ field as overridden.
47
+ """
48
+
49
+ assert copy.deepcopy(_UNSET) is _UNSET
50
+ assert copy.copy(_UNSET) is _UNSET
51
+
52
+
53
+ def test_backend_survives_deepcopy_before_finalization() -> None:
54
+ """Finalize identically after a deepcopy taken before the next ``_spec`` read.
55
+
56
+ Reproduces the HF ``PretrainedConfig.to_dict()`` path: overriding one field
57
+ (``target_clusters``) invalidates ``_finalized`` without touching the other
58
+ fields' still-``_UNSET`` raw slots. Deep-copying the backend in that state
59
+ (as upstream does for nested sub-configs, before their own ``to_dict()``
60
+ ever ran) must not corrupt those slots when the copy is finalized.
61
+ """
62
+
63
+ backend = MobilintAriesBackend()
64
+ backend.target_clusters = [0, 1]
65
+ assert backend._finalized is None
66
+
67
+ copied = copy.deepcopy(backend)
68
+
69
+ assert copied.to_dict() == backend.to_dict()
70
+
71
+
34
72
  def test_default_target_device_is_aries_rb() -> None:
35
73
  """Use Aries RB when callers do not declare a board."""
36
74
 
@@ -40,7 +78,10 @@ def test_default_target_device_is_aries_rb() -> None:
40
78
  assert backend.target_device == "aries-rb"
41
79
 
42
80
 
43
- @pytest.mark.parametrize("target_device", ["regulus-ra", "regulus-rb"])
81
+ @pytest.mark.parametrize(
82
+ "target_device",
83
+ ["regulus-ra", "regulus-rb", "regulus-ra-usb", "regulus-rb-usb"],
84
+ )
44
85
  def test_positional_target_device_selects_the_product_backend(
45
86
  target_device: str,
46
87
  ) -> None:
@@ -376,3 +417,145 @@ def test_regulus_auto_positional_empty_cluster_targets_are_canonicalized() -> No
376
417
  backend = MobilintRegulusBackend("", 0, "auto", None, [])
377
418
 
378
419
  assert backend.to_dict()["target_clusters"] == ["0:0"]
420
+
421
+
422
+ @pytest.mark.parametrize(
423
+ "target_device",
424
+ ["regulus-ra", "regulus-rb", "regulus-ra-usb", "regulus-rb-usb"],
425
+ )
426
+ def test_regulus_dev_no_sugar_expands_to_single_core(target_device: str) -> None:
427
+ """Expand Regulus ``dev_no`` sugar to its one-core topology, not Aries's grid.
428
+
429
+ Without target-device-aware sugar expansion, a Regulus config that only
430
+ names ``target_device`` receives Aries's 2 clusters × 4 cores default
431
+ and is then rejected by :class:`MobilintRegulusBackend`'s topology check.
432
+ Every Regulus board (PCIe and USB) must produce a single ``d:0:0`` core.
433
+ """
434
+
435
+ backend = MobilintNPUBackend(target_device=target_device)
436
+
437
+ assert isinstance(backend, MobilintRegulusBackend)
438
+ assert backend.to_dict()["target_cores"] == ["0:0:0"]
439
+
440
+
441
+ def test_regulus_dev_no_sugar_respects_device_list() -> None:
442
+ """Expand Regulus ``dev_no=[0, 1]`` sugar to one core per named device."""
443
+
444
+ backend = MobilintNPUBackend(target_device="regulus-rb-usb", dev_no=[0, 1])
445
+
446
+ assert set(backend.to_dict()["target_cores"]) == {"0:0:0", "1:0:0"}
447
+
448
+
449
+ @pytest.mark.parametrize(
450
+ "target_device",
451
+ ["regulus-ra", "regulus-rb", "regulus-ra-usb", "regulus-rb-usb"],
452
+ )
453
+ def test_from_kwargs_sugar_uses_regulus_topology(target_device: str) -> None:
454
+ """Populate ``dev_no`` sugar with Regulus topology at the config layer.
455
+
456
+ Direct :meth:`NPUTargetSpec.from_kwargs` exercises the sugar-expansion
457
+ branch that Model Zoo hits before the concrete backend __init__ can
458
+ populate its own single-core default: when the config layer sees only
459
+ ``target_device`` (no ``target_cores`` / ``target_clusters``), the spec
460
+ must emit ``["0:0:0"]`` for every Regulus board so a subsequent
461
+ :class:`MobilintRegulusBackend` construction accepts the pre-populated
462
+ grain.
463
+ """
464
+
465
+ spec = NPUTargetSpec.from_kwargs(
466
+ {"target_device": target_device, "core_mode": "single"}
467
+ )
468
+
469
+ assert list(spec.cores) == ["0:0:0"]
470
+
471
+
472
+ @pytest.mark.parametrize(
473
+ "target_device",
474
+ ["regulus-ra", "regulus-rb", "regulus-ra-usb", "regulus-rb-usb"],
475
+ )
476
+ def test_spec_with_override_preserves_regulus_topology(target_device: str) -> None:
477
+ """Keep a Regulus spec's board identity across a derived ``_with`` override.
478
+
479
+ Regression for the case where ``NPUTargetSpec.from_kwargs`` set
480
+ ``target_device`` only on the fresh pending and lost it on the returned
481
+ spec. A follow-up ``spec._with(dev_no=...)`` then created a new pending
482
+ with ``target_device=None``, and the sugar re-expansion produced Aries's
483
+ 2×4 grid, which :class:`MobilintRegulusBackend` would then reject for
484
+ referencing cores its single-cluster hardware does not have.
485
+ """
486
+
487
+ root = NPUTargetSpec.from_kwargs(
488
+ {"target_device": target_device, "core_mode": "single"}
489
+ )
490
+ assert root.target_device == target_device
491
+
492
+ derived = root._with(dev_no=1)
493
+
494
+ assert derived.target_device == target_device
495
+ assert list(derived.cores) == ["1:0:0"]
496
+
497
+
498
+ def test_from_kwargs_sugar_uses_aries_topology_by_default() -> None:
499
+ """Fall back to Aries 2×4 sugar when no ``target_device`` is declared.
500
+
501
+ Callers with legacy configs that never named a board must keep receiving
502
+ the historical 8-core Aries default; :func:`_topology_for_target` treats
503
+ ``None`` and any non-Regulus string as Aries.
504
+ """
505
+
506
+ spec = NPUTargetSpec.from_kwargs({"core_mode": "single"})
507
+
508
+ assert len(spec.cores) == 8
509
+ assert list(spec.cores) == [
510
+ "0:0:0",
511
+ "0:0:1",
512
+ "0:0:2",
513
+ "0:0:3",
514
+ "0:1:0",
515
+ "0:1:1",
516
+ "0:1:2",
517
+ "0:1:3",
518
+ ]
519
+
520
+
521
+ @pytest.mark.parametrize(
522
+ "target_device",
523
+ ["aries-rb", "regulus-ra", "regulus-rb", "regulus-ra-usb", "regulus-rb-usb"],
524
+ )
525
+ def test_create_passes_target_device_to_accelerator(
526
+ monkeypatch: pytest.MonkeyPatch, target_device: str
527
+ ) -> None:
528
+ """Forward the resolved board name to ``qbruntime.Accelerator``.
529
+
530
+ ``qbruntime>=1.4`` selects the physical device from the target-device
531
+ string passed as the first positional argument. Regressing that call
532
+ would silently strand USB and Regulus workloads on the default
533
+ accelerator, so pin the argument shape explicitly.
534
+ """
535
+
536
+ captured: list[tuple[Any, ...]] = []
537
+
538
+ class _Acc:
539
+ def __init__(self, *args: Any) -> None:
540
+ captured.append(args)
541
+
542
+ def dispose(self) -> None:
543
+ pass
544
+
545
+ class _StopCreate(Exception):
546
+ pass
547
+
548
+ def _no_model(*_args: Any, **_kwargs: Any) -> None:
549
+ raise _StopCreate
550
+
551
+ monkeypatch.setattr(npu_backend, "Accelerator", _Acc)
552
+ monkeypatch.setattr(npu_backend, "Model", _no_model)
553
+ monkeypatch.setattr(
554
+ npu_backend.MobilintNPUBackend, "check_model_path", lambda self, path: path
555
+ )
556
+
557
+ backend = MobilintNPUBackend(mxq_path="m.mxq", target_device=target_device, dev_no=0)
558
+ with pytest.raises(_StopCreate):
559
+ backend.create()
560
+
561
+ assert captured == [(target_device, 0)]
File without changes