diff --git a/README.md b/README.md index f0d2c2d3..11958998 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,8 @@ Use cases: agent dev loops (dictate a question, hear the answer in a cloned voic | Platform | Backend | Notes | | ------------------------ | -------------- | ---------------------------------------------- | | macOS (Apple Silicon) | MLX (Metal) | 4-5x faster via Neural Engine | -| Windows / Linux (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app | +| Windows (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app | +| Linux (NVIDIA) | PyTorch (CUDA) | Use a local/remote Python backend with CUDA PyTorch | | Linux (AMD) | PyTorch (ROCm) | Auto-configures HSA_OVERRIDE_GFX_VERSION | | Windows (any GPU) | DirectML | Universal Windows GPU support | | Intel Arc | IPEX/XPU | Intel discrete GPU acceleration | diff --git a/app/src/lib/api/types.ts b/app/src/lib/api/types.ts index 5fcec9ba..ba3ea87f 100644 --- a/app/src/lib/api/types.ts +++ b/app/src/lib/api/types.ts @@ -291,7 +291,10 @@ export interface CudaDownloadProgress { export interface CudaStatus { available: boolean; // CUDA binary exists on disk active: boolean; // Currently running the CUDA binary - binary_path?: string; + binary_path: string | null; + cuda_libs_version: string | null; + download_supported: boolean; // Platform has a matching release asset + unsupported_reason: string | null; downloading: boolean; // Download in progress download_progress?: CudaDownloadProgress; } diff --git a/backend/routes/cuda.py b/backend/routes/cuda.py index cd9d5766..cb1104eb 100644 --- a/backend/routes/cuda.py +++ b/backend/routes/cuda.py @@ -26,6 +26,10 @@ async def download_cuda_backend(): """Download the CUDA backend binary.""" from ..services import cuda + unsupported_reason = cuda.get_cuda_download_unsupported_reason() + if unsupported_reason: + raise HTTPException(status_code=409, detail=unsupported_reason) + if cuda.get_cuda_binary_path() is not None: raise HTTPException(status_code=409, detail="CUDA backend already downloaded") diff --git a/backend/services/cuda.py b/backend/services/cuda.py index 87fd8fb3..d274a43a 100644 --- a/backend/services/cuda.py +++ b/backend/services/cuda.py @@ -21,9 +21,9 @@ import tarfile from pathlib import Path from typing import Optional +from .. import __version__ from ..config import get_data_dir from ..utils.progress import get_progress_manager -from .. import __version__ logger = logging.getLogger(__name__) @@ -31,6 +31,8 @@ GITHUB_RELEASES_URL = "https://github.com/jamiepine/voicebox/releases/download" PROGRESS_KEY = "cuda-backend" +CUDA_DOWNLOAD_UNSUPPORTED_REASON = "Downloadable CUDA backend releases are currently only published for Windows." + # The current expected CUDA libs version. Bump this when we change the # CUDA toolkit version or torch's CUDA dependency changes (e.g. cu126 -> cu128). CUDA_LIBS_VERSION = "cu128-v1" @@ -63,6 +65,25 @@ def get_cuda_exe_name() -> str: return "voicebox-server-cuda" +def is_cuda_download_supported() -> bool: + """Return whether this platform has a matching CUDA release asset.""" + return sys.platform == "win32" + + +def get_cuda_download_unsupported_reason() -> str | None: + """Explain why this platform cannot use the release-download flow.""" + if is_cuda_download_supported(): + return None + return CUDA_DOWNLOAD_UNSUPPORTED_REASON + + +def ensure_cuda_download_supported() -> None: + """Raise if downloading would fetch an asset built for another platform.""" + reason = get_cuda_download_unsupported_reason() + if reason: + raise RuntimeError(reason) + + def get_cuda_binary_path() -> Optional[Path]: """Return path to the CUDA executable if it exists inside the onedir.""" p = get_cuda_dir() / get_cuda_exe_name() @@ -103,12 +124,15 @@ def get_cuda_status() -> dict: cuda_path = get_cuda_binary_path() progress = progress_manager.get_progress(PROGRESS_KEY) cuda_libs_version = get_installed_cuda_libs_version() + unsupported_reason = get_cuda_download_unsupported_reason() return { "available": cuda_path is not None, "active": is_cuda_active(), "binary_path": str(cuda_path) if cuda_path else None, "cuda_libs_version": cuda_libs_version, + "download_supported": unsupported_reason is None, + "unsupported_reason": unsupported_reason, "downloading": progress is not None and progress.get("status") == "downloading", "download_progress": progress, } @@ -257,6 +281,8 @@ async def download_cuda_binary(version: Optional[str] = None): async def _download_cuda_binary_locked(version: Optional[str] = None): """Inner implementation of download_cuda_binary, called under _download_lock.""" + ensure_cuda_download_supported() + import httpx if version is None: @@ -387,6 +413,11 @@ async def check_and_update_cuda_binary(): if not cuda_path: return # No CUDA binary installed, nothing to update + unsupported_reason = get_cuda_download_unsupported_reason() + if unsupported_reason: + logger.info("Skipping CUDA backend auto-update: %s", unsupported_reason) + return + need_server = _needs_server_download() need_libs = _needs_cuda_libs_download() diff --git a/backend/tests/test_cuda_download.py b/backend/tests/test_cuda_download.py new file mode 100644 index 00000000..77115f06 --- /dev/null +++ b/backend/tests/test_cuda_download.py @@ -0,0 +1,32 @@ +import sys as py_sys +import types + +import pytest + +from backend.services import cuda + + +def test_cuda_status_reports_unsupported_linux_download(monkeypatch, tmp_path): + monkeypatch.setattr(cuda.sys, "platform", "linux") + monkeypatch.setattr(cuda, "get_data_dir", lambda: tmp_path) + + status = cuda.get_cuda_status() + + assert status["available"] is False + assert status["download_supported"] is False + assert status["unsupported_reason"] == cuda.CUDA_DOWNLOAD_UNSUPPORTED_REASON + + +@pytest.mark.asyncio +async def test_cuda_download_rejects_linux_before_network(monkeypatch, tmp_path): + monkeypatch.setattr(cuda.sys, "platform", "linux") + monkeypatch.setattr(cuda, "get_data_dir", lambda: tmp_path) + + class UnexpectedClient: + def __init__(self, *args, **kwargs): + raise AssertionError("unsupported platforms should not start a release download") + + monkeypatch.setitem(py_sys.modules, "httpx", types.SimpleNamespace(AsyncClient=UnexpectedClient)) + + with pytest.raises(RuntimeError, match="currently only published for Windows"): + await cuda._download_cuda_binary_locked("v0.5.0") diff --git a/docs/content/docs/overview/gpu-acceleration.mdx b/docs/content/docs/overview/gpu-acceleration.mdx index b9730b7d..8d5d1a3b 100644 --- a/docs/content/docs/overview/gpu-acceleration.mdx +++ b/docs/content/docs/overview/gpu-acceleration.mdx @@ -23,7 +23,7 @@ This page is for the cases where it doesn't: | **Windows + NVIDIA** | PyTorch CUDA (cu128) | Auto-downloads the CUDA backend binary on first use | | **Windows + Intel Arc** | PyTorch XPU (IPEX) | New in 0.4 — works with Arc A-series and B-series | | **Windows generic GPU** | DirectML | Universal Windows GPU support; slower than CUDA | -| **Linux + NVIDIA** | PyTorch CUDA (cu128) | Same auto-download flow as Windows | +| **Linux + NVIDIA** | PyTorch CUDA (cu128) | Use a local/remote Python backend with CUDA PyTorch | | **Linux + AMD** | PyTorch ROCm | Auto-configures `HSA_OVERRIDE_GFX_VERSION` | | **Linux + Intel Arc** | PyTorch XPU (IPEX) | | | **Any (no GPU)** | PyTorch CPU | Works everywhere; expect 5-50x slower than GPU | @@ -46,7 +46,7 @@ On M-series Macs, Voicebox ships an MLX-optimized backend that uses the Apple Ne The Whisper Turbo + MLX combo dropped transcription latency from ~20s to ~2-3s on M-series chips (see CHANGELOG entry for v0.1.10). -## Windows / Linux + NVIDIA — The CUDA Backend Swap +## Windows + NVIDIA — The CUDA Backend Swap Voicebox doesn't bundle CUDA into the main installer (it would balloon downloads to multi-gigabyte territory for users who don't have an NVIDIA GPU). Instead, when you first need it, the app downloads a separate **CUDA backend binary** that contains the PyTorch + CUDA runtime. diff --git a/docs/content/docs/overview/introduction.mdx b/docs/content/docs/overview/introduction.mdx index de16aa18..155bdb41 100644 --- a/docs/content/docs/overview/introduction.mdx +++ b/docs/content/docs/overview/introduction.mdx @@ -75,7 +75,8 @@ No cloud fallback, no bring-your-own-API-key. Local is the product. | Platform | Backend | Notes | |----------|---------|-------| | macOS (Apple Silicon) | MLX (Metal) | 4-5x faster via Neural Engine | -| Windows / Linux (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app | +| Windows (NVIDIA) | PyTorch (CUDA) | Auto-downloads CUDA binary from within the app | +| Linux (NVIDIA) | PyTorch (CUDA) | Use a local/remote Python backend with CUDA PyTorch | | Linux (AMD) | PyTorch (ROCm) | Auto-configures HSA_OVERRIDE_GFX_VERSION | | Windows (any GPU) | DirectML | Universal Windows GPU support | | Intel Arc | IPEX/XPU | Intel discrete GPU acceleration |