Files
VoiceStudio/scripts/setup.py
T
72d137e1f3 fix(bootstrap): port cuDNN 8 (NVIDIA CUDA GPU) + VC++ redist to packaged installs (#869)
* fix(bootstrap): port cuDNN 8 (NVIDIA CUDA GPU) + VC++ redist install into ensure_venv_ready()

* fix(bootstrap): address #869 review — drop dead VC++ half, cache negative CUDA probe, gate on ROCm, sync docs

Per maintainer review on #869:

1. Drop the VC++ Redistributable half: LoadLibraryA("vcruntime140.dll")
   from the running Tauri exe is a tautology (the exe itself links the
   MSVC CRT, so the process wouldn't be running without it), and torch's
   real failure mode is msvcp140.dll inside the venv python process.
   Dead code removed; a comment records why for future readers.

2. Stop taxing every non-CUDA launch: a negative torch probe (CPU /
   Intel / AMD — most installs) is now cached in a
   .venv/.cudnn8_probe_negative marker, so the synchronous `import
   torch` runs at most once per venv lifetime. Invalidated on every
   path that can change the torch build (drift sync #307, repair sync,
   first-run sync, ROCm reinstall) and implicitly by a venv rebuild.
   A probe that fails to run cleanly is skipped WITHOUT caching so a
   transient error can't wedge a real CUDA machine.

3. Rewrite docs/install/troubleshooting.md §10 to the actual root
   cause: packaged installs never had the cudnn8_compat libs (so
   reinstalling never restored them); the bootstrap now installs them
   automatically on CUDA machines, with the manual uv pip command as
   the offline fallback and PyTorch Whisper as the sidestep.

4. Gate the ~700 MB nvidia-cudnn-cu12 download on the venv torch being
   a real CUDA build: the probe now reports 'hip' before checking
   cuda.is_available() (which HIP spoofs), so opt-in ROCm installs
   (#124) never fetch the CUDA wheel.

Also reflow the CHANGELOG entry to house style (bold one-line lead,
1-3 lines of why, (#827, #869) refs) and extend the bootstrap unit
tests: classify_cuda_probe verdict mapping and the marker
write/invalidate round-trip (6 cuDNN tests total, 43 lib tests green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:23:06 +05:30

194 lines
7.0 KiB
Python

#!/usr/bin/env python3
"""Post-install setup for platform-specific runtime dependencies.
1. **Windows: VC++ Redistributable** — PyTorch's native DLLs (c10.dll,
torch_cpu.dll, etc.) link against vcruntime140.dll and msvcp140.dll from
the Microsoft Visual C++ 2015-2022 Redistributable. Fresh Windows installs
(especially debloated/LTSC-style) don't ship it. We detect and auto-install
it silently before any `import torch` can fail.
2. **CUDA: cuDNN 8 compat** — Ensures cuDNN 8 libraries are available for
CTranslate2 (faster-whisper / WhisperX) alongside PyTorch 2.8+'s cuDNN 9.
Run automatically as part of `bun run setup:api` — no user action required.
Cross-platform:
- Linux: cuDNN 8 compat (.so.8 libs)
- Windows: VC++ Redistributable + cuDNN 8 compat (.dll libs)
- macOS: skipped (no CUDA)
"""
import os
import sys
import subprocess
import glob
# ── Windows: VC++ Redistributable ─────────────────────────────────────────
def _ensure_vcredist_windows():
"""Check for and install the VC++ 2015-2022 Redistributable on Windows.
PyTorch's native libraries (c10.dll, torch_cpu.dll, etc.) are built with
MSVC and dynamically link against vcruntime140.dll + msvcp140.dll. These
ship with Visual Studio / Build Tools but are NOT part of Windows itself.
On a fresh or debloated install the very first `import torch` crashes with:
OSError: [WinError 126] The specified module could not be found.
Error loading ...\\torch\\lib\\c10.dll or one of its dependencies.
This function silently downloads and installs the official x64 redist
package from Microsoft if the runtime DLLs are missing.
"""
if sys.platform != "win32":
return
# Check if vcruntime140.dll is already loadable
import ctypes
try:
ctypes.WinDLL("vcruntime140.dll")
print("✓ VC++ Redistributable: already installed")
return
except OSError:
pass
print("⚙ VC++ Redistributable not found — installing (required for PyTorch)...")
import tempfile
import urllib.request
vc_url = "https://aka.ms/vs/17/release/vc_redist.x64.exe"
installer = os.path.join(tempfile.gettempdir(), "vc_redist.x64.exe")
try:
# Download
print(" Downloading VC++ Redistributable...")
urllib.request.urlretrieve(vc_url, installer)
# Silent install (/install /quiet /norestart)
print(" Installing silently...")
result = subprocess.run(
[installer, "/install", "/quiet", "/norestart"],
timeout=120,
capture_output=True,
)
# Verify it worked
try:
ctypes.WinDLL("vcruntime140.dll")
print("✓ VC++ Redistributable: installed successfully")
except OSError:
# Exit code 3010 = success but reboot required
if result.returncode == 3010:
print("✓ VC++ Redistributable: installed (reboot recommended)")
else:
print(f"⚠ VC++ Redistributable: install may have failed (exit code {result.returncode})")
print(" Manual install: https://aka.ms/vs/17/release/vc_redist.x64.exe")
except Exception as e:
print(f"⚠ VC++ Redistributable: auto-install failed: {e}")
print(" Manual install: https://aka.ms/vs/17/release/vc_redist.x64.exe")
finally:
# Clean up installer
try:
os.remove(installer)
except OSError:
pass
# ── cuDNN 8 compat ────────────────────────────────────────────────────────
def _find_compat_dir():
"""Return the cudnn8_compat target directory, auto-detecting venv layout."""
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(script_dir)
venv_dir = os.path.join(project_root, ".venv")
if not os.path.isdir(venv_dir):
return None
if sys.platform == "win32":
# Windows: .venv/Lib/site-packages/
sp = os.path.join(venv_dir, "Lib", "site-packages", "cudnn8_compat")
else:
# Linux: .venv/lib/pythonX.Y/site-packages/
pyver = f"python{sys.version_info.major}.{sys.version_info.minor}"
sp = os.path.join(venv_dir, "lib", pyver, "site-packages", "cudnn8_compat")
return sp
def _cudnn8_lib_dir(compat_dir):
"""Return the cuDNN lib subdirectory within the compat install."""
if sys.platform == "win32":
return os.path.join(compat_dir, "nvidia", "cudnn", "bin")
return os.path.join(compat_dir, "nvidia", "cudnn", "lib")
def _count_cudnn8_libs(lib_dir):
"""Count cuDNN 8 shared libraries in the given directory."""
if sys.platform == "win32":
return len(glob.glob(os.path.join(lib_dir, "cudnn*64_8.dll")))
return len(glob.glob(os.path.join(lib_dir, "libcudnn*.so.8")))
def main():
# ── Step 1: Windows VC++ Redistributable ──────────────────────────────
_ensure_vcredist_windows()
# macOS — no CUDA, nothing to do
if sys.platform == "darwin":
return
compat_dir = _find_compat_dir()
if compat_dir is None:
return
lib_dir = _cudnn8_lib_dir(compat_dir)
# Already installed?
if os.path.isdir(lib_dir):
n = _count_cudnn8_libs(lib_dir)
if n >= 5:
print(f"✓ cuDNN 8 compat: {n} libraries ready")
return
# Check if CUDA is available before installing GPU-only libs
try:
result = subprocess.run(
[sys.executable, "-c", "import torch; print(torch.cuda.is_available())"],
capture_output=True, text=True, timeout=30,
)
if result.stdout.strip() != "True":
print("✓ No CUDA — cuDNN 8 compat not needed")
return
except Exception:
pass # Can't detect CUDA — install anyway, it's harmless on CPU
print("⚙ Installing cuDNN 8 compatibility libraries for CTranslate2...")
try:
# `uv venv` doesn't seed pip into the venv, so `sys.executable -m pip`
# fails with "No module named pip". `uv pip install --python` talks to
# the interpreter directly without needing pip installed inside it.
subprocess.run(
[
"uv", "pip", "install",
"--no-deps", "--target", compat_dir,
"--python", sys.executable,
"nvidia-cudnn-cu12==8.9.7.29",
],
check=True,
capture_output=True,
text=True,
timeout=180,
)
n = _count_cudnn8_libs(lib_dir)
print(f"✓ cuDNN 8 installed: {n} libraries")
except subprocess.CalledProcessError as e:
print(f"⚠ cuDNN 8 install failed (transcription may not work on CUDA):")
print(f" {(e.stderr or '')[:300]}")
except Exception as e:
print(f"⚠ cuDNN 8 install skipped: {e}")
if __name__ == "__main__":
main()