mblt-npu-python 0.0.2__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.2 → mblt_npu_python-0.1.0}/PKG-INFO +7 -5
  2. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/README.md +5 -3
  3. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu/__init__.py +1 -1
  4. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu/npu_backend.py +36 -6
  5. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu/npu_target.py +87 -6
  6. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/PKG-INFO +7 -5
  7. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/requires.txt +1 -1
  8. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/pyproject.toml +1 -1
  9. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/tests/test_npu_backend_multi_slot.py +3 -2
  10. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/tests/test_target_device.py +149 -2
  11. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/LICENSE +0 -0
  12. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu/core_mode.py +0 -0
  13. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu/logging.py +0 -0
  14. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu/onnx_backend.py +0 -0
  15. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu/pytest_plugin.py +0 -0
  16. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/SOURCES.txt +0 -0
  17. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/dependency_links.txt +0 -0
  18. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/mblt_npu_python.egg-info/top_level.txt +0 -0
  19. {mblt_npu_python-0.0.2 → mblt_npu_python-0.1.0}/setup.cfg +0 -0
  20. {mblt_npu_python-0.0.2 → 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.2
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.2"
25
+ __version__ = "0.1.0"
26
26
 
27
27
  __all__ = [
28
28
  "BACKEND_CLASSES",
@@ -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."""
@@ -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
@@ -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
 
@@ -131,6 +131,33 @@ _VALID_CLUSTER_INDICES: frozenset = frozenset(cluster_map.keys())
131
131
  _VALID_CORE_INDICES: frozenset = frozenset(core_map.keys())
132
132
 
133
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
+
134
161
  def _check_cluster_core_indices(c_val: int, k_val: int, entry: Any) -> None:
135
162
  """Raise ``ValueError`` when ``c_val`` / ``k_val`` fall outside the Aries2 topology."""
136
163
  if (
@@ -474,6 +501,16 @@ class NPUTargetSpec:
474
501
  _pending: Optional["NPUTargetSpecPending"] = field(
475
502
  default=None, compare=False, hash=False, repr=False
476
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
+ )
477
514
 
478
515
  @property
479
516
  def _dev_no_overridden(self) -> bool:
@@ -539,6 +576,7 @@ class NPUTargetSpec:
539
576
  dev_no_key = f"{prefix}dev_no"
540
577
  cores_key = f"{prefix}target_cores"
541
578
  clusters_key = f"{prefix}target_clusters"
579
+ target_device_key = f"{prefix}target_device"
542
580
 
543
581
  core_mode = normalize_core_mode(kwargs.get(core_mode_key, "single"))
544
582
  dev_no = _dedup_dev_no(kwargs.get(dev_no_key, _DEFAULT_DEV_NO))
@@ -546,6 +584,7 @@ class NPUTargetSpec:
546
584
  dev_list = _normalize_dev_list(dev_no)
547
585
  fallback_dev = dev_list[0]
548
586
  dev_no_given = dev_no_key in kwargs
587
+ target_device = kwargs.get(target_device_key)
549
588
 
550
589
  raw_cores = kwargs.get(cores_key)
551
590
  raw_clusters = kwargs.get(clusters_key)
@@ -558,6 +597,7 @@ class NPUTargetSpec:
558
597
  dev_no_given=dev_no_given,
559
598
  raw_cores=list(raw_cores) if raw_cores else [],
560
599
  raw_clusters=list(raw_clusters) if raw_clusters else [],
600
+ target_device=target_device,
561
601
  )
562
602
 
563
603
  # Mutate the caller's ``kwargs`` dict in place for downstream code
@@ -587,6 +627,7 @@ class NPUTargetSpec:
587
627
  core_mode=core_mode,
588
628
  cores=tuple(cores),
589
629
  clusters=tuple(clusters),
630
+ target_device=target_device,
590
631
  )
591
632
 
592
633
  def _with(
@@ -627,7 +668,10 @@ class NPUTargetSpec:
627
668
  :meth:`NPUTargetSpecPending.finalize`.
628
669
  """
629
670
  if self._pending is None:
630
- 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
+ )
631
675
  else:
632
676
  base_pending = self._pending
633
677
  new_pending = base_pending._with(
@@ -684,6 +728,7 @@ def _resolve_targets(
684
728
  dev_no_given: bool,
685
729
  raw_cores: List[Any],
686
730
  raw_clusters: List[Any],
731
+ target_device: Optional[str] = None,
687
732
  ) -> tuple[List[str], List[str]]:
688
733
  """Canonicalize a ``(dev_no, core_mode, cores, clusters)`` payload.
689
734
 
@@ -721,11 +766,24 @@ def _resolve_targets(
721
766
  )
722
767
 
723
768
  if not cores and not clusters:
724
- # ``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)
725
776
  if core_mode == "single":
726
- 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
+ ]
727
783
  else:
728
- 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
+ ]
729
787
  else:
730
788
  # Grain unification per core_mode.
731
789
  if core_mode == "single":
@@ -797,6 +855,13 @@ class NPUTargetSpecPending:
797
855
  strings, or canonical ``"d:c:k"`` strings), or :data:`_UNSET`.
798
856
  raw_clusters: The caller's raw ``target_clusters`` override, or
799
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.
800
865
  """
801
866
 
802
867
  baseline: NPUTargetSpec
@@ -804,9 +869,15 @@ class NPUTargetSpecPending:
804
869
  raw_core_mode: Any = _UNSET
805
870
  raw_cores: Any = _UNSET
806
871
  raw_clusters: Any = _UNSET
872
+ target_device: Optional[str] = None
807
873
 
808
874
  @classmethod
809
- 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":
810
881
  """Return a fresh pending baseline for the next override epoch.
811
882
 
812
883
  :class:`MobilintNPUBackend` calls this immediately after materializing
@@ -822,13 +893,21 @@ class NPUTargetSpecPending:
822
893
  spec: Canonical :class:`NPUTargetSpec` to seed the fresh pending's
823
894
  baseline with. Typically the finalized result of the previous
824
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.
825
901
 
826
902
  Returns:
827
903
  A new :class:`NPUTargetSpecPending` whose baseline is ``spec``
828
904
  (with any prior ``_pending`` history stripped) and whose intent
829
905
  slots are all :data:`_UNSET`.
830
906
  """
831
- 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
+ )
832
911
 
833
912
  def _with(
834
913
  self,
@@ -962,6 +1041,7 @@ class NPUTargetSpecPending:
962
1041
  dev_no_given=dev_no_overridden,
963
1042
  raw_cores=raw_cores,
964
1043
  raw_clusters=raw_clusters,
1044
+ target_device=self.target_device,
965
1045
  )
966
1046
 
967
1047
  # Resolve the effective ``dev_no`` for the returned canonical spec.
@@ -983,6 +1063,7 @@ class NPUTargetSpecPending:
983
1063
  cores=tuple(cores),
984
1064
  clusters=tuple(clusters),
985
1065
  _pending=self,
1066
+ target_device=self.target_device,
986
1067
  )
987
1068
 
988
1069
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mblt-npu-python
3
- Version: 0.0.2
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
 
@@ -10,7 +10,7 @@ import pytest
10
10
 
11
11
  from mblt_npu import MobilintAriesBackend, MobilintNPUBackend, MobilintRegulusBackend
12
12
  import mblt_npu.npu_backend as npu_backend
13
- from mblt_npu.npu_target import _UNSET
13
+ from mblt_npu.npu_target import _UNSET, NPUTargetSpec
14
14
 
15
15
 
16
16
  @pytest.mark.parametrize(
@@ -19,6 +19,8 @@ from mblt_npu.npu_target import _UNSET
19
19
  ("aries-rb", MobilintAriesBackend),
20
20
  ("regulus-ra", MobilintRegulusBackend),
21
21
  ("regulus-rb", MobilintRegulusBackend),
22
+ ("regulus-ra-usb", MobilintRegulusBackend),
23
+ ("regulus-rb-usb", MobilintRegulusBackend),
22
24
  ],
23
25
  )
24
26
  def test_target_device_selects_the_product_backend(
@@ -76,7 +78,10 @@ def test_default_target_device_is_aries_rb() -> None:
76
78
  assert backend.target_device == "aries-rb"
77
79
 
78
80
 
79
- @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
+ )
80
85
  def test_positional_target_device_selects_the_product_backend(
81
86
  target_device: str,
82
87
  ) -> None:
@@ -412,3 +417,145 @@ def test_regulus_auto_positional_empty_cluster_targets_are_canonicalized() -> No
412
417
  backend = MobilintRegulusBackend("", 0, "auto", None, [])
413
418
 
414
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