Add the ability to download a CUDA-enabled backend binary (~2.4 GB) and swap it in via a backend-only restart, solving the #1 user pain point (19 open 'GPU not detected' issues caused by GitHub's 2 GB asset limit). Backend: - cuda_download.py: download from R2 (primary) or GitHub split-parts (fallback), SHA-256 verification, atomic writes, progress via SSE - 4 new endpoints: GET/POST/DELETE /backend/cuda-*, GET cuda-progress - server.py: --version flag, auto-detect variant from binary name - build_binary.py: --cuda flag for CUDA PyInstaller builds - split_binary.py: split large binaries into <2GB GitHub Release assets - CI workflow for building CUDA binary Tauri: - restart_server command (stop -> wait -> start) - start_server prefers CUDA binary from {data_dir}/backends/ if present - Version mismatch check: runs --version before launching CUDA binary Frontend: - GpuAcceleration component: download, progress, restart, switch, delete - API client + types for CUDA status and management - Platform lifecycle: restartServer() on Tauri/Web - Aggressive 1s health polling during restart for fast reconnection
8.3 KiB
CUDA Backend Swap — Implementation Summary
Status: Complete | Branch:
feat/cuda-backend-swap| Created: 2026-03-12
What This Is
A standalone feature that lets users download a CUDA-enabled backend binary (~2.4 GB) and swap it in via a backend-only restart. The frontend stays running, all UI state is preserved. This solves the #1 user pain point: 19 open issues about "GPU not detected" caused by GitHub's 2 GB release asset limit preventing CUDA binaries from shipping in official releases.
How It Works
User clicks "Download CUDA Backend" in Settings
→ Backend fetches manifest from GitHub Releases
→ Downloads split parts (<2 GB each), concatenates them
→ SHA-256 integrity check on reassembled binary
→ Binary placed in {app_data_dir}/backends/voicebox-server-cuda
→ User clicks "Switch to CUDA Backend"
→ Tauri kills CPU process, launches CUDA binary, frontend reconnects
→ On all future app launches, CUDA binary is auto-detected and used
The CUDA binary is functionally identical to the CPU binary — same FastAPI app, same endpoints, same code. The only difference is PyTorch compiled with CUDA 12.1 and bundled CUDA runtime libraries.
Architecture Decisions
Backend-only restart, not full app restart. The Tauri shell kills the current voicebox-server process, waits 1 second for port release, and spawns the new binary. The React frontend stays running. Health polling detects the new backend within seconds.
No provider/subprocess architecture. This is explicitly not the PR #33 approach (10K+ lines, 136 files, 22 bugs). One process at a time. The CUDA binary replaces the CPU binary — it doesn't run alongside it.
Data directory, not app bundle. The CUDA binary lives in {app_data_dir}/backends/, which persists across app updates and avoids code-signing issues. The bundled CPU binary in the app bundle is untouched.
Version mismatch protection. On startup, Rust runs voicebox-server-cuda --version and compares to the app version from tauri.conf.json. If they don't match (e.g., after an app update), it falls back to the bundled CPU binary silently.
GitHub Releases distribution. The CUDA binary is split into <2 GB chunks (GitHub's asset limit) via scripts/split_binary.py. The app downloads a manifest, fetches each part, concatenates them, and runs a SHA-256 integrity check to verify reassembly. No external hosting needed.
Files Changed
New Files
| File | Lines | Purpose |
|---|---|---|
backend/cuda_download.py |
~190 | Download split parts from GitHub Releases, reassemble, verify integrity |
scripts/split_binary.py |
~80 | Split large binary into <2 GB chunks with SHA-256 manifest |
.github/workflows/build-cuda.yml |
~70 | CI workflow: build CUDA binary, split, upload to GitHub Releases |
app/src/components/ServerSettings/GpuAcceleration.tsx |
371 | GPU Acceleration UI card (status, download, restart, delete) |
docs/plans/CUDA_BACKEND_SWAP.md |
581 | Original implementation plan (5 phases with code sketches) |
docs/plans/CUDA_BACKEND_SWAP_FINAL.md |
this file | Final implementation summary |
docs/plans/PROJECT_STATUS.md |
462 | Full project triage (all PRs, issues, architecture) |
docs/plans/PR33_CUDA_PROVIDER_REVIEW.md |
~350 | Detailed code review of PR #33 (22 bugs documented) |
Modified Files
| File | What Changed |
|---|---|
backend/build_binary.py |
Added --cuda flag, parameterized output binary name |
backend/server.py |
Added --version flag, auto-detect backend variant from binary name (VOICEBOX_BACKEND_VARIANT env var) |
backend/main.py |
4 new endpoints (/backend/cuda-status, /backend/download-cuda, /backend/cuda, /backend/cuda-progress), health endpoint returns backend_variant |
backend/models.py |
HealthResponse model: added backend_variant field |
backend/requirements.txt |
Added httpx>=0.27.0 for async HTTP downloads |
tauri/src-tauri/src/main.rs |
restart_server command (stop → wait → start), start_server checks for CUDA binary in data dir and launches via shell().command(), version mismatch check |
app/src/platform/types.ts |
PlatformLifecycle.restartServer() added |
tauri/src/platform/lifecycle.ts |
restartServer() implementation via invoke('restart_server') |
web/src/platform/lifecycle.ts |
restartServer() noop for web platform |
app/src/lib/api/types.ts |
CudaStatus, CudaDownloadProgress interfaces; HealthResponse updated with gpu_type, backend_type, backend_variant |
app/src/lib/api/client.ts |
getCudaStatus(), downloadCudaBackend(), deleteCudaBackend() methods |
app/src/components/ServerTab/ServerTab.tsx |
Wired in <GpuAcceleration /> component (Tauri-only) |
Backend API Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/backend/cuda-status |
Returns { available, active, binary_path, downloading, download_progress } |
POST |
/backend/download-cuda |
Starts background download; returns immediately. Track via SSE. |
DELETE |
/backend/cuda |
Deletes CUDA binary (blocked if CUDA is currently active) |
GET |
/backend/cuda-progress |
SSE stream of download progress (reuses existing ProgressManager) |
The existing GET /health endpoint now returns two new fields:
backend_type:"pytorch"or"mlx"(existing detection)backend_variant:"cpu"or"cuda"(set fromVOICEBOX_BACKEND_VARIANTenv var)
Frontend UI States
The GpuAcceleration card in Server Settings handles these states:
- Native GPU detected (MPS, MLX, XPU, DirectML) — Shows info message, no download needed
- No CUDA binary — Download button with size estimate, description of requirements
- Downloading — SSE-driven progress bar with bytes/total and percentage
- Downloaded, not active — "Switch to CUDA Backend" button + "Remove" option
- CUDA active — Shows CUDA badge, "Switch to CPU Backend" button
- Restarting — Spinner with phase text, 1s health polling as safety net
- Error — Red error message with details
Key UX detail: switching to CPU
Since start_server always prefers the CUDA binary if it exists on disk, "Switch to CPU" must delete the CUDA binary first, then restart. The user can re-download later. This avoids a persistent configuration mechanism (no new state to manage, no new config file, no DB column).
Rust: Server Lifecycle
start_server
├── Check for CUDA binary at {data_dir}/backends/voicebox-server-cuda
├── If found: run --version, compare to app version
│ ├── Match: launch via shell().command() with --data-dir, --port
│ └── Mismatch: log warning, fall through to CPU
└── Else: launch bundled sidecar via shell().sidecar()
restart_server
├── stop_server (kill process tree)
├── wait 1 second for port release
└── start_server (auto-detects CUDA)
What This Doesn't Cover
- AMD GPU / ROCm / DirectML binary — Same pattern, different PyTorch build. Future PR.
- Linux CUDA — Same approach, just another CI matrix entry. Can ship same release.
- Multi-model support — LuxTTS, Chatterbox, etc. are a separate architectural concern (in-process model registry). Independent of binary variant.
- Download resume — If download is interrupted, it restarts from scratch. Acceptable for v1.
- Remote server CUDA — Users running voicebox-server on a remote machine manage their own binaries. This feature is for the desktop app.
Testing Checklist
- Build CUDA binary locally with
python backend/build_binary.py --cuda voicebox-server-cuda --versionprints correct version- Place CUDA binary in
{data_dir}/backends/, launch app → auto-detects and uses it - Version mismatch: rename binary to have wrong version → falls back to CPU
- Frontend: GpuAcceleration card shows correct state for CPU, CUDA available, CUDA active
- Download flow: POST triggers download, SSE progress works, completion updates status
- Switch to CUDA: restart works, health endpoint shows
backend_variant: "cuda" - Switch to CPU: deletes binary, restarts, health shows
backend_variant: "cpu" - Delete CUDA while active: returns 409 error
- Split binary script:
python scripts/split_binary.pycreates manifest + parts + sha256 - Native GPU (macOS MPS): shows info message, no download section