From 88be097b62631d500045526dd04510ed2e23fbc4 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Tue, 17 Mar 2026 04:57:05 -0700 Subject: [PATCH] fix: update package_cuda.py for PyInstaller 6.18 layout and remove split_binary.py - Fix is_nvidia_file() to match NVIDIA DLLs in _internal/torch/lib/ (PyInstaller 6.18 + torch 2.10 no longer uses nvidia/ subdirectories) - Remove deprecated split_binary.py (both archives are under 2GB) - Update torch_compat range to >=2.6.0,<2.11.0 - Update build docs for new dual-archive packaging flow --- .github/workflows/release.yml | 2 +- docs/content/docs/developer/building.mdx | 12 ++-- docs/plans/CUDA_LIBS_ADDON.md | 2 +- scripts/package_cuda.py | 64 ++++++++++++----- scripts/split_binary.py | 88 ------------------------ 5 files changed, 54 insertions(+), 114 deletions(-) delete mode 100644 scripts/split_binary.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef1a662f..bc60e8db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -212,7 +212,7 @@ jobs: backend/dist/voicebox-server-cuda/ \ --output release-assets/ \ --cuda-libs-version cu126-v1 \ - --torch-compat ">=2.6.0,<2.8.0" + --torch-compat ">=2.6.0,<2.11.0" - name: Upload archives to GitHub Release if: startsWith(github.ref, 'refs/tags/') diff --git a/docs/content/docs/developer/building.mdx b/docs/content/docs/developer/building.mdx index 0b3593ad..5ea6c747 100644 --- a/docs/content/docs/developer/building.mdx +++ b/docs/content/docs/developer/building.mdx @@ -159,12 +159,14 @@ Tauri looks for `voicebox-server-${PLATFORM}` in `src-tauri/binaries/` and bundl The `build-cuda-windows` job runs separately: -1. Install PyTorch with CUDA 12.1 -2. Build with `build_binary.py --cuda` -3. Split binary with `scripts/split_binary.py` -4. Upload parts as release artifacts +1. Install PyTorch with CUDA 12.6 +2. Build with `build_binary.py --cuda` (produces `--onedir` output) +3. Package with `scripts/package_cuda.py` into two archives: + - `voicebox-server-cuda.tar.gz` — server core (~945 MB) + - `cuda-libs-cu126-v1.tar.gz` — NVIDIA runtime libraries (~1.7 GB, cached independently) +4. Upload archives as release artifacts -This binary is downloaded on-demand by users who enable CUDA in settings. +This binary is downloaded on-demand by users who enable CUDA in settings. The CUDA libs archive is only re-downloaded when the CUDA toolkit version changes, not on every app update. ## Troubleshooting diff --git a/docs/plans/CUDA_LIBS_ADDON.md b/docs/plans/CUDA_LIBS_ADDON.md index 440d7950..28cbe1b6 100644 --- a/docs/plans/CUDA_LIBS_ADDON.md +++ b/docs/plans/CUDA_LIBS_ADDON.md @@ -170,4 +170,4 @@ PyInstaller onedir creates a parent bootloader + child Python process on Windows 5. Update `main.rs`: change launch path to `backends/cuda/` dir + add `.current_dir()` 6. Add `ensure_cuda_structure()` helper in Rust to verify exe + nvidia/ subdirs exist before spawning 7. Update CI pipeline: `build-cuda-windows` produces two archives instead of split parts -8. Update `split_binary.py` or replace with archive-based distribution +8. ~~Update `split_binary.py` or replace with archive-based distribution~~ Done: replaced with `package_cuda.py` diff --git a/scripts/package_cuda.py b/scripts/package_cuda.py index 550f80d6..45b07836 100644 --- a/scripts/package_cuda.py +++ b/scripts/package_cuda.py @@ -19,41 +19,67 @@ import sys import tarfile from pathlib import Path -# Directories / prefixes that belong in the CUDA libs archive. -# PyInstaller --onedir puts NVIDIA packages in nvidia/ subdirectories -# (e.g. nvidia/cublas/lib/, nvidia/cudnn/lib/, etc.) -NVIDIA_PREFIXES = ( - "nvidia/", - "nvidia\\", -) - -# Individual DLL patterns that may end up at the top level on Windows +# DLL name prefixes that identify NVIDIA CUDA runtime libraries. +# These DLLs may appear in different locations depending on the torch +# and PyInstaller version: +# - nvidia/ subdirectories (older torch with separate nvidia-* packages) +# - _internal/torch/lib/ (torch 2.10+ bundles NVIDIA DLLs directly) +# - Top-level directory (some PyInstaller versions) NVIDIA_DLL_PREFIXES = ( "cublas", + "cublaslt", "cudart", "cudnn", "cufft", + "cufftw", "curand", "cusolver", + "cusolvermg", "cusparse", "nvjitlink", "nvrtc", + "nccl", + "caffe2_nvrtc", ) +# Files to keep in the server core even if they match NVIDIA prefixes. +# These are small Python modules or stubs, not the large runtime DLLs. +NVIDIA_KEEP_IN_CORE = { + "torch/cuda/nccl.py", + "torch/_inductor/codegen/cuda/cutlass_lib_extensions/cutlass_mock_imports/cuda/cudart.py", +} + def is_nvidia_file(rel_path: str) -> bool: - """Check if a relative path belongs to the NVIDIA CUDA libs.""" + """Check if a relative path belongs to the NVIDIA CUDA libs. + + Identifies large NVIDIA runtime DLLs (.dll/.so) regardless of where + PyInstaller placed them. Excludes small Python stubs that happen to + share NVIDIA-related names. + """ rel_lower = rel_path.lower().replace("\\", "/") - # Files under nvidia/ subdirectory tree - if rel_lower.startswith("nvidia/"): - return True + # Never split out Python source files or small stubs + if rel_lower in NVIDIA_KEEP_IN_CORE: + return False - # Top-level NVIDIA DLLs (Windows) — e.g. cublas64_12.dll - name = rel_lower.rsplit("/", 1)[-1] - for prefix in NVIDIA_DLL_PREFIXES: - if name.startswith(prefix) and (name.endswith(".dll") or name.endswith(".so")): + # Files under nvidia/ subdirectory tree (older torch layout) + if rel_lower.startswith("nvidia/") or "nvidia/" in rel_lower.split("/", 1)[-1:]: + # Only DLLs/shared objects — not .py, .dist-info, etc. + if rel_lower.endswith((".dll", ".so")): return True + # Include entire nvidia/ namespace package tree + for part in rel_lower.split("/"): + if part == "nvidia": + return True + + # NVIDIA DLLs anywhere in the tree (e.g. _internal/torch/lib/cublas64_12.dll) + name = rel_lower.rsplit("/", 1)[-1] + if name.endswith(".dll") or name.endswith(".so"): + name_no_ext = name.rsplit(".", 1)[0] + for prefix in NVIDIA_DLL_PREFIXES: + if name_no_ext.startswith(prefix): + return True return False @@ -186,8 +212,8 @@ def main(): parser.add_argument( "--torch-compat", type=str, - default=">=2.6.0,<2.8.0", - help="Torch version compatibility range (default: >=2.6.0,<2.8.0)", + default=">=2.6.0,<2.11.0", + help="Torch version compatibility range (default: >=2.6.0,<2.11.0)", ) args = parser.parse_args() diff --git a/scripts/split_binary.py b/scripts/split_binary.py deleted file mode 100644 index ee959daa..00000000 --- a/scripts/split_binary.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Split a large binary into chunks for GitHub Releases (<2 GB each). - -DEPRECATED: For CUDA builds, use scripts/package_cuda.py instead. -This script was used when the CUDA binary was built with --onefile and -needed to be split into parts for the 2GB GitHub Release asset limit. -With the switch to --onedir + dual archives (server core + CUDA libs), -package_cuda.py handles the packaging. - -Usage: - python scripts/split_binary.py backend/dist/voicebox-server-cuda.exe - python scripts/split_binary.py backend/dist/voicebox-server-cuda.exe --chunk-size 1900000000 - python scripts/split_binary.py backend/dist/voicebox-server-cuda.exe --output release-assets/ - -The script produces: - - voicebox-server-cuda.part00.exe, .part01.exe, ... (binary chunks) - - voicebox-server-cuda.sha256 (SHA-256 checksum of the complete file) - - voicebox-server-cuda.manifest (ordered list of part filenames) -""" - -import argparse -import hashlib -import sys -from pathlib import Path - - -def split(input_path: Path, chunk_size: int, output_dir: Path): - output_dir.mkdir(parents=True, exist_ok=True) - data = input_path.read_bytes() - total_size = len(data) - - # Write SHA-256 of the complete file - sha256 = hashlib.sha256(data).hexdigest() - checksum_file = output_dir / f"{input_path.stem}.sha256" - checksum_file.write_text(f"{sha256} {input_path.name}\n") - - # Split into chunks - parts = [] - for i in range(0, total_size, chunk_size): - part_index = len(parts) - part_name = f"{input_path.stem}.part{part_index:02d}{input_path.suffix}" - part_path = output_dir / part_name - part_path.write_bytes(data[i : i + chunk_size]) - parts.append(part_name) - - # Write manifest (ordered list of part filenames) - manifest_file = output_dir / f"{input_path.stem}.manifest" - manifest_file.write_text("\n".join(parts) + "\n") - - print(f"Input: {input_path} ({total_size / (1024**3):.2f} GB)") - print(f"Output: {output_dir}/") - print(f"Parts: {len(parts)} (chunk size: {chunk_size / (1024**3):.2f} GB)") - print(f"SHA-256: {sha256}") - print(f"Manifest: {manifest_file.name}") - for p in parts: - size = (output_dir / p).stat().st_size - print(f" {p} ({size / (1024**3):.2f} GB)") - - -def main(): - parser = argparse.ArgumentParser( - description="Split a large binary into chunks for GitHub Releases" - ) - parser.add_argument("input", type=Path, help="Path to the binary file to split") - parser.add_argument( - "--chunk-size", - type=int, - default=1_900_000_000, # 1.9 GB — safely under 2 GB GitHub limit - help="Maximum chunk size in bytes (default: 1.9 GB)", - ) - parser.add_argument( - "--output", - type=Path, - default=None, - help="Output directory (default: same directory as input)", - ) - args = parser.parse_args() - - if not args.input.exists(): - print(f"Error: {args.input} does not exist", file=sys.stderr) - sys.exit(1) - - output_dir = args.output or args.input.parent - split(args.input, args.chunk_size, output_dir) - - -if __name__ == "__main__": - main()