Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 98 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ jobs:
python scripts/package_cuda.py \
backend/dist/voicebox-server-cuda/ \
--output release-assets/ \
--platform windows-x86_64 \
--cuda-libs-version cu128-v1 \
--torch-compat ">=2.7.0,<2.11.0"

Expand All @@ -325,10 +326,10 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: |
release-assets/voicebox-server-cuda.tar.gz
release-assets/voicebox-server-cuda.tar.gz.sha256
release-assets/cuda-libs-cu128-v1.tar.gz
release-assets/cuda-libs-cu128-v1.tar.gz.sha256
release-assets/voicebox-server-cuda-windows-x86_64.tar.gz
release-assets/voicebox-server-cuda-windows-x86_64.tar.gz.sha256
release-assets/cuda-libs-windows-x86_64-cu128-v1.tar.gz
release-assets/cuda-libs-windows-x86_64-cu128-v1.tar.gz.sha256
release-assets/cuda-libs.json
draft: true
env:
Expand All @@ -341,6 +342,94 @@ jobs:
path: backend/dist/voicebox-server-cuda/
retention-days: 7

build-gpu-linux:
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- backend: cuda
torch_index: https://download.pytorch.org/whl/cu128
torch_attr: cuda
libs_version: cu128-v1
torch_compat: ">=2.7.0,<2.11.0"
# Ampere through Blackwell; +PTX keeps forward-compat for newer arches.
arch_list: "8.0;8.6;8.9;9.0;12.0+PTX"
- backend: rocm
torch_index: https://download.pytorch.org/whl/rocm7.2
torch_attr: hip
libs_version: rocm7.2-v1
torch_compat: ">=2.12.0,<2.13.0"
arch_list: ""

steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- name: Free up disk space
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
tool-cache: true
large-packages: false

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Install GPU PyTorch (Linux) first so requirements see it satisfied
# The Linux GPU runtime ships inside the torch wheels (for ROCm:
# torch/lib/*.so + rocBLAS/hipBLASLt/aotriton kernel data — unlike
# Windows, which uses separate rocm_sdk wheels). Installing it before
# requirements.txt keeps pip from first pulling the default torch stack
# that requirements.txt's unqualified torch would otherwise resolve —
# avoiding wasted downloads and disk pressure.
run: |
python -m pip install --upgrade pip
pip install pyinstaller
pip install torch torchaudio \
--index-url ${{ matrix.torch_index }}

- name: Install remaining Python dependencies
run: |
pip install -r backend/requirements.txt
pip install --no-deps chatterbox-tts
pip install --no-deps hume-tada

- name: Verify GPU support in torch
run: python -c "import torch; assert torch.version.${{ matrix.torch_attr }}, 'torch is not a ${{ matrix.backend }} build'; print(torch.__version__)"

- name: Build GPU server binary (onedir)
working-directory: backend
env:
TORCH_CUDA_ARCH_LIST: ${{ matrix.arch_list }}
run: python build_binary.py --${{ matrix.backend }}

- name: Package into server core + GPU libs archives
run: |
python scripts/package_${{ matrix.backend }}.py \
backend/dist/voicebox-server-${{ matrix.backend }}/ \
--output release-assets/ \
--platform linux-x86_64 \
--${{ matrix.backend }}-libs-version ${{ matrix.libs_version }} \
--torch-compat "${{ matrix.torch_compat }}"

- name: Upload archives to GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: |
release-assets/voicebox-server-${{ matrix.backend }}-linux-x86_64.tar.gz
release-assets/voicebox-server-${{ matrix.backend }}-linux-x86_64.tar.gz.sha256
release-assets/${{ matrix.backend }}-libs-linux-x86_64-${{ matrix.libs_version }}.tar.gz
release-assets/${{ matrix.backend }}-libs-linux-x86_64-${{ matrix.libs_version }}.tar.gz.sha256
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

build-rocm-windows:
runs-on: windows-latest
permissions:
Expand Down Expand Up @@ -378,6 +467,7 @@ jobs:
python scripts/package_rocm.py \
backend/dist/voicebox-server-rocm/ \
--output release-assets/ \
--platform windows-x86_64 \
--rocm-libs-version rocm7.2-v1 \
--torch-compat ">=2.9.0,<2.10.0"

Expand All @@ -386,10 +476,10 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: |
release-assets/voicebox-server-rocm.tar.gz
release-assets/voicebox-server-rocm.tar.gz.sha256
release-assets/rocm-libs-rocm7.2-v1.tar.gz
release-assets/rocm-libs-rocm7.2-v1.tar.gz.sha256
release-assets/voicebox-server-rocm-windows-x86_64.tar.gz
release-assets/voicebox-server-rocm-windows-x86_64.tar.gz.sha256
release-assets/rocm-libs-windows-x86_64-rocm7.2-v1.tar.gz
release-assets/rocm-libs-windows-x86_64-rocm7.2-v1.tar.gz.sha256
release-assets/rocm-libs.json
draft: true
env:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
Ubuntu render/video group IDs; the container joins the groups that own the GPU
device nodes at startup. Native Linux setup now picks ROCm wheels for AMD GPUs
and CUDA wheels for NVIDIA GPUs before installing backend dependencies.
- **Downloadable GPU backends install a native Linux binary, not a Windows
`.exe`.** Installing the CUDA or ROCm backend on Linux previously downloaded a
Windows executable and silently fell back to CPU, because the release assets
weren't platform-specific and only Windows binaries were published. GPU server
assets are now platform-qualified (`voicebox-server-{cuda,rocm}-<platform>.tar.gz`)
and new `build-cuda-linux` / `build-rocm-linux` release jobs build the Linux
bundles (ROCm from the PyTorch ROCm index, whose wheels bundle the
HIP/rocBLAS/hipBLASLt/aotriton runtime). The ROCm download is now offered on
Linux hosts with an AMD ROCm driver (`/dev/kfd`), not only on Windows. Fixes
the Linux CUDA-installs-a-Windows-exe bug.

## [0.5.0] - 2026-04-22

Expand Down
4 changes: 2 additions & 2 deletions app/src/components/ServerTab/GpuPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ export function GpuPage() {
const cudaDownloading = cudaStatus?.downloading ?? false;
const rocmAvailable = rocmStatus?.available ?? false;
const rocmDownloading = rocmStatus?.downloading ?? false;
// The ROCm backend only applies to AMD GPUs on Windows. Show the section when
// the backend detects applicable hardware, or it is already downloaded/active.
// The ROCm backend applies to AMD GPUs on Windows or Linux (/dev/kfd). Show the
// section when the backend detects applicable hardware, or it is already downloaded/active.
const supportsRocm = (health?.supports_rocm ?? false) || rocmAvailable || isCurrentlyRocm;

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export interface HealthResponse {
vram_used_mb?: number;
backend_type?: string;
backend_variant?: string; // "cpu", "cuda", or "rocm"
supports_rocm?: boolean; // AMD GPU on Windows — the ROCm backend is applicable
supports_rocm?: boolean; // AMD GPU on Windows or Linux (/dev/kfd) — the ROCm backend is applicable
}

export interface CudaDownloadProgress {
Expand Down
8 changes: 7 additions & 1 deletion backend/build_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,13 @@ def build_server(cuda=False, rocm=False):
)
args.extend(gpu_hidden)

if rocm:
if rocm and platform.system() == "Windows":
# Windows delivers the ROCm runtime as separate rocm_sdk wheels (there is
# no system ROCm install). On Linux the download.pytorch.org/whl/rocm
# torch wheels bundle the HIP/MIOpen/rocBLAS .so runtime inside
# torch/lib/ directly, so none of the rocm_sdk collection below applies —
# PyInstaller's torch hook picks those .so up automatically.
#
# rocm_sdk imports its backend packages dynamically via
# importlib.import_module(py_package_name), which PyInstaller's
# static analyzer cannot see. We must collect them explicitly —
Expand Down
2 changes: 1 addition & 1 deletion backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ class HealthResponse(BaseModel):
vram_used_mb: Optional[float] = None
backend_type: Optional[str] = None # Backend type (mlx or pytorch)
backend_variant: Optional[str] = None # Binary variant (cpu, cuda, or rocm)
supports_rocm: bool = False # AMD GPU on Windows — the ROCm backend is applicable
supports_rocm: bool = False # AMD GPU on Windows or Linux (/dev/kfd) — ROCm backend applicable
gpu_compatibility_warning: Optional[str] = None # Warning if GPU arch unsupported


Expand Down
4 changes: 2 additions & 2 deletions backend/routes/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .. import config, models
from ..services import tts
from ..database import get_db
from ..utils.platform_detect import get_backend_type, is_amd_gpu_windows
from ..utils.platform_detect import get_backend_type, is_amd_rocm_capable

router = APIRouter()

Expand Down Expand Up @@ -186,7 +186,7 @@ async def health():
vram_used_mb=vram_used,
backend_type=backend_type,
backend_variant=os.environ.get("VOICEBOX_BACKEND_VARIANT", default_variant),
supports_rocm=is_amd_gpu_windows(),
supports_rocm=is_amd_rocm_capable(),
gpu_compatibility_warning=gpu_compat_warning,
)

Expand Down
11 changes: 9 additions & 2 deletions backend/services/cuda.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from typing import Optional

from ..config import get_data_dir
from ..utils.platform_detect import SUPPORTED_GPU_ASSET_PLATFORMS, server_asset_platform
from ..utils.progress import get_progress_manager
from .. import __version__

Expand Down Expand Up @@ -286,8 +287,14 @@ async def _download_cuda_binary_locked(version: Optional[str] = None):
)

base_url = f"{GITHUB_RELEASES_URL}/{version}"
server_archive = "voicebox-server-cuda.tar.gz"
libs_archive = f"cuda-libs-{CUDA_LIBS_VERSION}.tar.gz"
plat = server_asset_platform()
if plat not in SUPPORTED_GPU_ASSET_PLATFORMS:
raise RuntimeError(
f"CUDA backend not available for platform '{plat}'. "
f"Supported platforms: {', '.join(SUPPORTED_GPU_ASSET_PLATFORMS)}"
)
server_archive = f"voicebox-server-cuda-{plat}.tar.gz"
libs_archive = f"cuda-libs-{plat}-{CUDA_LIBS_VERSION}.tar.gz"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

try:
async with httpx.AsyncClient(follow_redirects=True, timeout=30.0) as client:
Expand Down
6 changes: 4 additions & 2 deletions backend/services/rocm.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from typing import Optional

from ..config import get_data_dir
from ..utils.platform_detect import server_asset_platform
from ..utils.progress import get_progress_manager
from .. import __version__

Expand Down Expand Up @@ -287,8 +288,9 @@ async def _download_rocm_binary_locked(version: Optional[str] = None):
# release tag; the libs content version is encoded in the filename only.
server_base_url = f"{GITHUB_RELEASES_URL}/{version}"
libs_base_url = server_base_url
server_archive = "voicebox-server-rocm.tar.gz"
libs_archive = f"rocm-libs-{ROCM_LIBS_VERSION}.tar.gz"
plat = server_asset_platform()
server_archive = f"voicebox-server-rocm-{plat}.tar.gz"
libs_archive = f"rocm-libs-{plat}-{ROCM_LIBS_VERSION}.tar.gz"

# Always stage when any download is needed, then atomically rename over
# rocm_dir on success. This prevents a failed mid-extraction from leaving
Expand Down
70 changes: 67 additions & 3 deletions backend/tests/test_amd_gpu_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@

import pytest

from backend.utils.platform_detect import is_amd_gpu_windows
from backend.utils.platform_detect import (
is_amd_gpu_windows,
is_amd_rocm_capable,
server_asset_platform,
)


class TestAmdGpuWindows:
Expand Down Expand Up @@ -43,8 +47,13 @@ def test_detects_amd_via_wmi(self, _mock_run, _mock_system):
"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."""
@patch("torch.cuda.is_available", return_value=False)
def test_no_amd_via_wmi(self, _mock_avail, _mock_run, _mock_system):
"""WMI reporting zero AMD adapters should return False.

torch.cuda.is_available is pinned False so the fallback probe can't pick
up real AMD hardware when this runs on an actual ROCm host.
"""
assert is_amd_gpu_windows() is False

@patch("backend.utils.platform_detect.platform.system", return_value="Windows")
Expand Down Expand Up @@ -94,3 +103,58 @@ 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


class TestServerAssetPlatform:
"""Pin the exact release-asset platform tokens the download URLs depend on."""

@pytest.mark.parametrize(
"system,machine,expected",
[
("Linux", "x86_64", "linux-x86_64"),
("Windows", "AMD64", "windows-x86_64"),
("Linux", "aarch64", "linux-arm64"),
("Darwin", "arm64", "darwin-arm64"),
],
)
def test_tokens(self, system, machine, expected):
with (
patch("backend.utils.platform_detect.platform.system", return_value=system),
patch("backend.utils.platform_detect.platform.machine", return_value=machine),
):
assert server_asset_platform() == expected


class TestAmdRocmCapable:
"""Linux ROCm capability gate (drives the ROCm download UI)."""

@pytest.fixture(autouse=True)
def _clear_cache(self):
is_amd_rocm_capable.cache_clear()
yield
is_amd_rocm_capable.cache_clear()

@patch("backend.utils.platform_detect.platform.machine", return_value="x86_64")
@patch("backend.utils.platform_detect.platform.system", return_value="Linux")
@patch("backend.utils.platform_detect.os.path.exists", return_value=True)
def test_linux_x86_with_kfd_is_capable(self, _mock_exists, _mock_system, _mock_machine):
"""/dev/kfd present => ROCm-capable, even before ROCm torch is installed."""
assert is_amd_rocm_capable() is True

@patch("backend.utils.platform_detect.platform.machine", return_value="x86_64")
@patch("backend.utils.platform_detect.platform.system", return_value="Linux")
@patch("backend.utils.platform_detect.os.path.exists", return_value=False)
def test_linux_without_kfd_is_not_capable(self, _mock_exists, _mock_system, _mock_machine):
assert is_amd_rocm_capable() is False

@patch("backend.utils.platform_detect.platform.machine", return_value="aarch64")
@patch("backend.utils.platform_detect.platform.system", return_value="Linux")
@patch("backend.utils.platform_detect.os.path.exists", return_value=True)
def test_linux_arm_is_not_offered_unbuilt_asset(self, _mock_exists, _mock_system, _mock_machine):
"""ARM has /dev/kfd but no published asset — must not be offered a 404."""
assert is_amd_rocm_capable() is False

@patch("backend.utils.platform_detect.platform.machine", return_value="arm64")
@patch("backend.utils.platform_detect.platform.system", return_value="Darwin")
def test_macos_is_not_capable(self, _mock_system, _mock_machine):
assert is_amd_rocm_capable() is False
Loading
Loading