mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 12:50:42 -07:00
* feat(windows): add native ROCm support for AMD GPUs Implements native ROCm architecture for Windows. - Adds backend build pipeline for voicebox-server-rocm.exe - Detects AMD GPUs dynamically and routes PyTorch allocations - Adds automatic download and update logic for ROCm dependencies - Refactors UI in GpuPage.tsx and GpuAcceleration.tsx to add AMD flows - Fixes 'Switch to CPU' lock on Windows via Tauri backend_override state - Resolves PyInstaller/rocm_sdk UnboundLocalError silent crashes - Resolves Numba/NumPy 2.x incompatibilities during Qwen3-TTS load - Resolves HF_HUB_OFFLINE Catch-22 for CustomVoice processor caching * fix(rocm): host libs archive under the app release tag, drop offline-load regression Align the ROCm libs download with the CUDA pattern: both the server core and the libs archive are published under the app-version release tag, with the libs content version encoded in the filename only. The previous code fetched libs from a separate rocm7.2-v1 tag, which disagreed with the download test. Also revert the unrelated Qwen CustomVoice changes that wrapped model loading in force_offline_if_cached (not imported — a NameError on load for every platform) and re-added a Base-model cache gate. The inference-path offline guard was deliberately removed previously. * feat(rocm): gate download on AMD detection and persist the backend variant The ROCm download section now only shows when the backend reports an AMD GPU on Windows (new supports_rocm health field, backed by the memoized is_amd_gpu_windows detection that was previously unused), or when ROCm is already downloaded/active. Make the backend override honor a pinned variant: set_backend_override persists the choice to disk so it survives an app restart, start_server reads it back, and a cuda/rocm pin now actually selects that variant instead of always preferring ROCm. A stale pin to a deleted backend self-heals to the default order rather than forcing CPU. Add the web no-op stub for the new method. * chore(rocm): drop incomplete vitest harness for the unused GpuAcceleration component GpuAcceleration.tsx is not routed anywhere (GpuPage is the live settings view), and the added vitest setup referenced testing-library/vitest deps that were not in the lockfile, breaking the web typecheck. Remove the dead component's test and its scaffolding to keep this PR scoped to the ROCm feature. * ci(rocm): add ROCm release-artifact pipeline Mirror the CUDA packaging path for ROCm so the runtime download has artifacts to fetch. scripts/package_rocm.py splits the PyInstaller --rocm onedir into voicebox-server-rocm.tar.gz (core) + rocm-libs-rocm7.2-v1.tar.gz (AMD runtime: HIP DLLs, rocBLAS Tensile data, MIOpen kernel DBs) + rocm-libs.json, matching the names services/rocm.py expects, both under the app-version release tag. The new build-rocm-windows job in release.yml builds on windows-latest/cp312 and lets build_binary.py --rocm pull the official AMD Radeon wheels. The file classifier can't be validated against a real AMD build on CI, so it has unit coverage (test_package_rocm.py) against a synthetic onedir layout. The prefixes/dir markers may need a tweak after the first real build on AMD hardware — the packager hard-fails loudly if it classifies zero ROCm files. --------- Co-authored-by: Jamie Pine <[email protected]>
97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
"""
|
|
Phase 2.1 Test: AMD GPU detection on Windows.
|
|
|
|
Validates is_amd_gpu_windows() via mocked WMI and torch queries.
|
|
|
|
Usage:
|
|
python -m pytest backend/tests/test_amd_gpu_detect.py -v
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from backend.utils.platform_detect import is_amd_gpu_windows
|
|
|
|
|
|
class TestAmdGpuWindows:
|
|
"""Unit tests for is_amd_gpu_windows with mocks."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_detection_cache(self):
|
|
# is_amd_gpu_windows is memoized; reset between cases so each mock takes effect.
|
|
is_amd_gpu_windows.cache_clear()
|
|
yield
|
|
is_amd_gpu_windows.cache_clear()
|
|
|
|
@patch("backend.utils.platform_detect.platform.system", return_value="Linux")
|
|
def test_returns_false_on_linux(self, _mock_system):
|
|
"""Non-Windows platforms should always return False."""
|
|
assert is_amd_gpu_windows() is False
|
|
|
|
@patch("backend.utils.platform_detect.platform.system", return_value="Windows")
|
|
@patch(
|
|
"backend.utils.platform_detect.subprocess.run",
|
|
return_value=MagicMock(stdout="1\n", returncode=0),
|
|
)
|
|
def test_detects_amd_via_wmi(self, _mock_run, _mock_system):
|
|
"""WMI reporting an AMD adapter should return True."""
|
|
assert is_amd_gpu_windows() is True
|
|
|
|
@patch("backend.utils.platform_detect.platform.system", return_value="Windows")
|
|
@patch(
|
|
"backend.utils.platform_detect.subprocess.run",
|
|
return_value=MagicMock(stdout="0\n", returncode=0),
|
|
)
|
|
def test_no_amd_via_wmi(self, _mock_run, _mock_system):
|
|
"""WMI reporting zero AMD adapters should return False."""
|
|
assert is_amd_gpu_windows() is False
|
|
|
|
@patch("backend.utils.platform_detect.platform.system", return_value="Windows")
|
|
@patch(
|
|
"backend.utils.platform_detect.subprocess.run",
|
|
side_effect=Exception("WMI not available"),
|
|
)
|
|
@patch("torch.cuda.is_available", return_value=True)
|
|
@patch(
|
|
"torch.cuda.get_device_name",
|
|
return_value="AMD Radeon RX 7800 XT",
|
|
)
|
|
def test_fallback_to_torch_radeon(self, _mock_name, _mock_avail, _mock_run, _mock_system):
|
|
"""When WMI fails, torch.cuda.get_device_name('Radeon') should return True."""
|
|
assert is_amd_gpu_windows() is True
|
|
|
|
@patch("backend.utils.platform_detect.platform.system", return_value="Windows")
|
|
@patch(
|
|
"backend.utils.platform_detect.subprocess.run",
|
|
side_effect=Exception("WMI not available"),
|
|
)
|
|
@patch("torch.cuda.is_available", return_value=True)
|
|
@patch(
|
|
"torch.cuda.get_device_name",
|
|
return_value="NVIDIA GeForce RTX 4090",
|
|
)
|
|
def test_fallback_to_torch_nvidia(self, _mock_name, _mock_avail, _mock_run, _mock_system):
|
|
"""When WMI fails, torch.cuda.get_device_name('NVIDIA') should return False."""
|
|
assert is_amd_gpu_windows() is False
|
|
|
|
@patch("backend.utils.platform_detect.platform.system", return_value="Windows")
|
|
@patch(
|
|
"backend.utils.platform_detect.subprocess.run",
|
|
side_effect=Exception("WMI not available"),
|
|
)
|
|
@patch("torch.cuda.is_available", return_value=False)
|
|
def test_no_torch_cuda(self, _mock_avail, _mock_run, _mock_system):
|
|
"""When WMI fails and torch.cuda is unavailable, should return False."""
|
|
assert is_amd_gpu_windows() is False
|
|
|
|
@patch("backend.utils.platform_detect.platform.system", return_value="Windows")
|
|
@patch(
|
|
"backend.utils.platform_detect.subprocess.run",
|
|
side_effect=Exception("WMI not available"),
|
|
)
|
|
def test_torch_not_installed(self, _mock_run, _mock_system):
|
|
"""When torch is not installed, should return False without crashing."""
|
|
with patch.dict("sys.modules", {"torch": None}):
|
|
assert is_amd_gpu_windows() is False
|