fix(moss): align accelerator detection and routing status

This commit is contained in:
Palash Debnath
2026-09-07 11:13:33 +05:30
parent a671004cb7
commit cd58bbded0
34 changed files with 135 additions and 27 deletions
+2
View File
@@ -20,6 +20,8 @@ the frozen-backend fallback mirror it for their toolchains.
### Fixed
- MOSS accelerator routing now matches runtime device selection, including native XPU and registered NPU detection (#1830) — thanks @li-lizhe!
## [0.5.2] — 2026-09-02
+1 -1
View File
@@ -150,7 +150,7 @@ def _compute_device_state() -> dict:
caps = device_caps.detect_host_caps()
env_pin = (os.environ.get("OMNIVOICE_DEVICE") or "").strip().lower()
auto_family = next(
(f for f in ("cuda", "rocm", "xpu", "mps") if f in caps.available_families),
(f for f in device_caps.ACCELERATOR_PRIORITY if f in caps.available_families),
"cpu",
)
value = device_caps.requested_device_override()
+22 -4
View File
@@ -35,7 +35,8 @@ import sys
from dataclasses import dataclass
from typing import Literal
DeviceFamily = Literal["cuda", "rocm", "mps", "xpu", "cpu"]
DeviceFamily = Literal["cuda", "rocm", "mps", "xpu", "npu", "cpu"]
ACCELERATOR_PRIORITY = ("cuda", "rocm", "xpu", "npu", "mps")
# Stable substring stamped onto notes that represent a real kernel-launch risk
# (arch/driver mismatch) — as opposed to advisory notes (multi-GPU, VRAM query
@@ -533,9 +534,12 @@ def _probe() -> HostCaps:
# is the whole truth in that case (CodeRabbit, #1425).
notes.extend(why_no_gpu(torch))
# ── Intel XPU via IPEX ───────────────────────────────────────────────
# Older builds register XPU through IPEX; modern torch exposes it directly.
try:
import intel_extension_for_pytorch # noqa: F401
except Exception:
pass
try:
if hasattr(torch, "xpu") and torch.xpu.is_available():
detected.append("xpu")
if not device_name:
@@ -546,7 +550,21 @@ def _probe() -> HostCaps:
pass
notes.append("XPU VRAM not queried (unreliable across IPEX versions)")
except Exception:
# IPEX absent or XPU probe failed — no XPU on this host.
# XPU probe failed — no usable XPU on this host.
pass
# Vendor extensions may register an NPU with torch. Probe only an already
# registered backend; never install or import an optional vendor package.
try:
if hasattr(torch, "npu") and torch.npu.is_available():
detected.append("npu")
if not device_name:
try:
device_name = torch.npu.get_device_name(0)
except Exception:
pass
notes.append("NPU VRAM not queried")
except Exception:
pass
# ── Apple Silicon MPS ────────────────────────────────────────────────
@@ -579,7 +597,7 @@ def _probe() -> HostCaps:
# Preferred family by priority; cpu when nothing accelerated was detected.
family: DeviceFamily = "cpu"
for pref in ("cuda", "rocm", "xpu", "mps"):
for pref in ACCELERATOR_PRIORITY:
if pref in detected:
family = pref # type: ignore[assignment]
break
+6 -10
View File
@@ -29,15 +29,11 @@ Do NOT import ``main.py`` from the parent process — it runs under a
different venv (``transformers==5.0.0``) and importing it in-process would
re-introduce the exact conflict this isolation exists to avoid.
Hardware honesty (cross-platform rule): MOSS-TTS-v1.5's upstream documents
only CUDA and CPU. There is **no documented or tested MPS path** — the
custom ``trust_remote_code`` modelling code and the separate audio
tokenizer are unverified on Apple Silicon. We therefore advertise
``gpu_compat = ("cuda", "cpu")`` and the sidecar selects ``cuda`` when
present else ``cpu`` — it never silently routes to MPS where it might
crash. On Apple Silicon the engine honestly resolves to CPU (slow but
correct), and the engine is opt-in regardless, so it never becomes a
broken default on any platform.
Hardware routing follows the sidecar's runtime-available PyTorch accelerator:
CUDA/ROCm, XPU, or a registered NPU. MPS remains excluded; CPU is the fallback.
XPU/NPU routing is covered with mocked device contracts, not physical-hardware
synthesis certification; users need a compatible torch/vendor runtime in the
isolated engine venv.
"""
from __future__ import annotations
@@ -91,7 +87,7 @@ class MossTTSV15Backend(SubprocessBackend):
_DEFAULT_SAMPLE_RATE = 24000
# Honest hardware surface: upstream documents CUDA + CPU only. MPS is
# undocumented / untested, so we do NOT claim it (cross-platform rule).
gpu_compat = ("cuda", "cpu")
gpu_compat = ("cuda", "rocm", "xpu", "npu", "cpu")
# ── availability ───────────────────────────────────────────────────────
+1 -1
View File
@@ -2371,7 +2371,7 @@ def list_backends() -> list[dict]:
"one_click_install": bool, # services.sidecar_install can provision it in-app
"last_error": Optional[str], # cached most-recent failure
"isolation_mode": "in-process" | "subprocess",
"gpu_compat": list[str], # subset of {cuda, rocm, mps, xpu, cpu}
"gpu_compat": list[str], # subset of {cuda, rocm, mps, xpu, npu, cpu}
"supports_cloning": Optional[bool], # True/False from the class attr; None when
# model-dependent (property, e.g. mlx-audio)
"effective_device": str, # device this engine uses on THIS host
+5 -4
View File
@@ -23,10 +23,11 @@ interpreter, so MOSS runs behind
8 GB GPUs when quantized; the bf16 Transformers path used here is ~16 GB
of weights, so a 16 GB+ GPU is the realistic CUDA target. It also runs on
**CPU** (fp32) — correct but slow.
- **Device:** CUDA when present, else CPU. **There is no MPS path**
upstream documents only CUDA/CPU and the custom modelling code is
untested on Apple Silicon, so VoiceStudio never routes MOSS to MPS. On a
Mac it runs on CPU.
- **Device:** the sidecar uses a runtime-available PyTorch CUDA/ROCm, XPU,
or registered NPU backend, otherwise CPU. The isolated engine venv needs the
matching torch/vendor integration. MPS still uses CPU. XPU/NPU routing is
covered by mocked loader tests; physical-device synthesis has not been
validated by this change.
## Install
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "تعذّر تحميل إعداد الجهاز",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Geräteeinstellung konnte nicht geladen werden",
+1
View File
@@ -934,6 +934,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Failed to load device setting",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "No se pudo cargar el ajuste del dispositivo",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Impossible de charger le réglage du périphérique",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "डिवाइस सेटिंग लोड नहीं हो सकी",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Gagal memuat pengaturan perangkat",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Impossibile caricare l'impostazione del dispositivo",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "デバイス設定を読み込めませんでした",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "장치 설정을 불러오지 못했습니다",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Apparaatinstelling kon niet worden geladen",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Nie udało się wczytać ustawienia urządzenia",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Falha ao carregar a configuração do dispositivo",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Не удалось загрузить настройку устройства",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Kunde inte läsa in enhetsinställningen",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "โหลดการตั้งค่าอุปกรณ์ไม่สำเร็จ",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Aygıt ayarı yüklenemedi",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Не вдалося завантажити налаштування пристрою",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "Không tải được cài đặt thiết bị",
+1
View File
@@ -586,6 +586,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "无法加载设备设置",
+1
View File
@@ -334,6 +334,7 @@
"device_family_cuda": "NVIDIA GPU (CUDA)",
"device_family_rocm": "AMD GPU (ROCm)",
"device_family_xpu": "Intel GPU (XPU)",
"device_family_npu": "NPU",
"device_family_mps": "Apple GPU (MPS)",
"device_family_cpu": "CPU",
"device_load_failed": "無法載入裝置設定",
@@ -82,7 +82,7 @@ _REQUIRED_KEYS = {
"effective_device", "routing_status", "routing_reason",
}
_TTS_ASR_STATUSES = {"accelerated", "cpu_fallback", "cpu_only", "unavailable"}
_VALID_FAMILIES = {"cuda", "rocm", "mps", "xpu", "cpu"}
_VALID_FAMILIES = {"cuda", "rocm", "mps", "xpu", "npu", "cpu"}
def test_engines_response_includes_new_fields(fresh_app):
@@ -109,3 +109,14 @@ def test_env_pin_is_reported_and_wins(fresh_app, monkeypatch):
r = c.put("/api/settings/compute-device", json={"value": "auto"})
assert r.status_code == 200
assert r.json()["value"] == "cpu"
def test_auto_summary_reports_registered_npu(fresh_app, monkeypatch):
from core import device_caps
monkeypatch.setattr(device_caps, 'detect_host_caps', lambda: device_caps.HostCaps(
family='npu', available_families=('npu', 'cpu'),
))
body = _client(fresh_app).get('/api/settings/compute-device').json()
assert body['auto_family'] == body['effective_family'] == 'npu'
# Detection does not globally opt unrelated engines into NPU execution.
assert 'npu' not in body['choices']
+1 -1
View File
@@ -34,7 +34,7 @@ _EXPECTED = {
# is_available — e.g. parakeet-mlx gates on Apple Silicon via mlx_supported()).
_GPU_ONLY: set[str] = {"parakeet-mlx"}
_VALID = {"cuda", "rocm", "mps", "xpu", "cpu"}
_VALID = {"cuda", "rocm", "mps", "xpu", "npu", "cpu"}
def _cls(engine_id):
+21
View File
@@ -235,3 +235,24 @@ def test_result_is_cached_until_refresh():
def teardown_module(_module):
# Drop any cached mock-derived result so other test modules re-probe clean.
device_caps.detect_host_caps.cache_clear()
def test_builtin_xpu_does_not_require_ipex():
caps = _probe_with({'torch': _torch_mock(xpu_available=True), 'intel_extension_for_pytorch': None})
assert caps.family == 'xpu'
def test_registered_npu_is_reported_without_importing_vendor_packages():
torch = _torch_mock()
torch.npu = types.SimpleNamespace(is_available=lambda: True, get_device_name=lambda i: 'Ascend')
caps = _probe_with({'torch': torch, 'intel_extension_for_pytorch': None})
assert caps.family == 'npu'
assert caps.available_families == ('npu', 'cpu')
assert caps.device_name == 'Ascend'
def test_unavailable_npu_does_not_claim_acceleration():
torch = _torch_mock()
torch.npu = types.SimpleNamespace(is_available=lambda: False)
caps = _probe_with({'torch': torch, 'intel_extension_for_pytorch': None})
assert caps.family == 'cpu'
+1 -1
View File
@@ -132,7 +132,7 @@ def test_empty_compat_is_defensive_cpu_only():
# ── Contract guarantees ───────────────────────────────────────────────────
def test_never_emits_n_a():
for fam in ("cuda", "rocm", "mps", "xpu", "cpu"):
for fam in ("cuda", "rocm", "mps", "xpu", "npu", "cpu"):
for compat in ((), ("cpu",), ("cuda",), ("cuda", "cpu"), ("mps", "cpu")):
assert resolve_routing(compat, _caps(fam))["routing_status"] != "n/a"
+41 -3
View File
@@ -106,12 +106,12 @@ def test_audited_custom_remote_code_requires_both_opt_ins(monkeypatch):
# ── hardware honesty (cross-platform rule) ─────────────────────────────────
def test_gpu_compat_cuda_cpu_no_mps():
def test_gpu_compat_matches_accelerator_paths_without_mps():
"""MPS is undocumented/untested upstream — we must not claim it."""
from engines.moss_tts_v15 import MossTTSV15Backend
assert MossTTSV15Backend.gpu_compat == ("cuda", "cpu"), (
f"expected ('cuda', 'cpu'), got {MossTTSV15Backend.gpu_compat!r}"
assert MossTTSV15Backend.gpu_compat == ("cuda", "rocm", "xpu", "npu", "cpu"), (
f"unexpected device targets: {MossTTSV15Backend.gpu_compat!r}"
)
@@ -199,3 +199,41 @@ def test_generate_without_ref_audio_omits_reference(monkeypatch):
MossTTSV15Backend().generate("just text")
assert "ref_audio" not in captured
assert "tokens" not in captured
@pytest.mark.parametrize('family', [None, 'cuda', 'xpu', 'npu', 'mps'])
def test_loader_device_matches_routing(monkeypatch, family):
"""Exercise model + tokenizer placement without importing optional weights."""
import io
from types import SimpleNamespace
from unittest.mock import Mock
from engines.moss_tts_v15 import main, MossTTSV15Backend
from core.device_caps import HostCaps
from services.engine_routing import resolve_routing
expected = family if family not in (None, 'mps') else 'cpu'
accelerator = Mock(return_value=SimpleNamespace(type=family) if family else None)
monkeypatch.setitem(sys.modules, 'torch', SimpleNamespace(
accelerator=SimpleNamespace(current_accelerator=accelerator),
bfloat16='bf16', float32='fp32',
))
processor = Mock()
processor.model_config.sampling_rate = 24000
tokenizer = processor.audio_tokenizer
model = Mock()
model.to.return_value = model
factory = Mock()
factory.from_pretrained.return_value = model
monkeypatch.setitem(sys.modules, 'transformers', SimpleNamespace(
AutoModel=factory,
AutoProcessor=SimpleNamespace(from_pretrained=lambda *a, **kw: processor),
))
monkeypatch.setattr(main, '_state', None)
monkeypatch.setattr(main, '_model_source', lambda: ('local-fixture', 'a' * 40))
state = main._load_model(io.BytesIO())
accelerator.assert_called_once_with(check_available=True)
model.to.assert_called_once_with(expected)
tokenizer.to.assert_called_once_with(expected)
assert factory.from_pretrained.call_args.kwargs['torch_dtype'] == ('fp32' if expected == 'cpu' else 'bf16')
caps = HostCaps(family=family or 'cpu', available_families=(family, 'cpu') if family else ('cpu',))
assert resolve_routing(MossTTSV15Backend.gpu_compat, caps)['effective_device'] == state[2]
+1 -1
View File
@@ -145,7 +145,7 @@ def test_preflight_device_summary(client):
assert d["gpu_backend"] in {"cuda", "rocm", "mps", "cpu"}
assert d["gpu_vendor"] in {"nvidia", "amd", "apple", "intel", "unknown", "none"}
# #21: canonical-probe family + VRAM joined the device summary.
assert d["gpu_family"] in {"cuda", "rocm", "mps", "xpu", "cpu"}
assert d["gpu_family"] in {"cuda", "rocm", "mps", "xpu", "npu", "cpu"}
assert isinstance(d["vram_gb"], (int, float))