Files
VoiceStudio/tests/test_mlx_supported.py
T
Palash DebnathandClaude Opus 4.8 e61665fe34 feat(routing): host device probe + routing resolver (#21 PR 1/5) (#430)
* feat(routing): canonical host device probe + routing resolver (#21 PR 1/5)

Foundational, backend-only slice of the GPU compatibility matrix (#21).
No API or UI change — wiring lands in PRs 3–5.

- `core/device_caps.py`: single source of truth for host accelerator
  capability. `detect_host_caps()` distinguishes ROCm from CUDA (unlike
  the gguf hardware_probe), never raises, makes no network call, stays
  kernel-free on cold start, and caches per process. Enumerates the full
  degradation contract (torch-unimportable→probe_ok=False, CUDA-init
  raises, device_count==0, multi-GPU, mem_get_info failure, arch
  mismatch, MPS, XPU, DirectML). Plus shared `mlx_supported()` gate
  (#390 groundwork) — exact-string platform check, no regex.
- `services/engine_routing.py`: pure `resolve_routing(gpu_compat, caps)`
  → `{effective_device, routing_status, routing_reason}`; deterministic
  and byte-identical across OSes. Rules for accelerated / cpu_fallback
  (the no-silent-fallback signal) / cpu_only / unavailable, incl. the
  ROCm-not-in-set, DirectML-neutral, and XPU edges.
- `get_best_device()` delegates its family decision to the probe so the
  loader and probe can never disagree; keeps the ROCm HSA env override
  and DirectML device-string return (probe reads, loader writes). String
  contract unchanged.
- 39 unit tests (probe / resolver / mlx gate / reason-scrub contract);
  no new regex (CodeQL-clean), English-only (CJK guard green).

The gguf hardware_probe rebase is a deliberate follow-up: it has its own
torch-mocked suite and a VRAM-driven quant table unaffected by the family
rename, so it stays out of this zero-risk slice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(routing): address review — full available_families + empty-except comments

CodeRabbit / CodeQL review on PR 1:
- `available_families` no longer drops secondary accelerators on hybrid hosts
  (e.g. NVIDIA + Intel-iGPU-via-IPEX). The probe now detects every accelerator
  independently and picks `family` by priority at the end, instead of
  short-circuiting after the first hit. Routing is unaffected (it keys off
  `family`), but the field is now honest. + hybrid-host test.
- Annotated every `except: pass` in device_caps with an explanatory comment
  (CodeQL py/empty-except).
- Removed the unused `_MIN_NVIDIA_DRIVER` constant — the driver-version check
  stays in wizard preflight (no subprocess on the probe path); documented why.
- `get_best_device()` now checks MPS before DirectML, mirroring the probe's
  family-priority order so loader and probe never disagree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:03:22 +05:30

79 lines
2.5 KiB
Python

"""Unit tests for core.device_caps.mlx_supported() — the shared MLX platform
gate (#390). Gates strictly on darwin+arm64+torch-MPS; returns the exact pinned
(ok, reason) tuples for every host so a stray mlx wheel on Linux/Windows/mac-Intel
never reports available.
"""
from __future__ import annotations
import sys
import types
from unittest.mock import patch
from core import device_caps
def _mps_torch(available):
return types.SimpleNamespace(
backends=types.SimpleNamespace(
mps=types.SimpleNamespace(is_available=lambda: available)
)
)
def test_apple_silicon_with_mps_supported():
with patch.object(sys, "platform", "darwin"), \
patch("platform.machine", return_value="arm64"), \
patch.dict("sys.modules", {"torch": _mps_torch(True)}):
ok, reason = device_caps.mlx_supported()
assert ok is True
assert reason == ""
def test_apple_silicon_without_mps():
with patch.object(sys, "platform", "darwin"), \
patch("platform.machine", return_value="arm64"), \
patch.dict("sys.modules", {"torch": _mps_torch(False)}):
ok, reason = device_caps.mlx_supported()
assert ok is False
assert "torch MPS unavailable" in reason
def test_mac_intel_rejected():
with patch.object(sys, "platform", "darwin"), \
patch("platform.machine", return_value="x86_64"):
ok, reason = device_caps.mlx_supported()
assert ok is False
assert reason == "MLX requires Apple Silicon; this Mac is Intel"
def test_linux_rejected_before_import():
with patch.object(sys, "platform", "linux"), \
patch("platform.machine", return_value="x86_64"):
ok, reason = device_caps.mlx_supported()
assert ok is False
assert "this host is linux/x86_64" in reason
def test_windows_rejected():
with patch.object(sys, "platform", "win32"), \
patch("platform.machine", return_value="AMD64"):
ok, reason = device_caps.mlx_supported()
assert ok is False
assert "MLX requires Apple Silicon" in reason
def test_apple_silicon_torch_unimportable():
real_import = __import__
def _blocked(name, *a, **k):
if name == "torch":
raise ImportError("no torch")
return real_import(name, *a, **k)
with patch.object(sys, "platform", "darwin"), \
patch("platform.machine", return_value="arm64"), \
patch("builtins.__import__", _blocked):
ok, reason = device_caps.mlx_supported()
assert ok is False
assert "torch not importable" in reason