diff --git a/.github/workflows/publish-hf-wheels.yml b/.github/workflows/publish-hf-wheels.yml new file mode 100644 index 0000000..4aaf4f4 --- /dev/null +++ b/.github/workflows/publish-hf-wheels.yml @@ -0,0 +1,206 @@ +name: Publish Hugging Face Wheels + +"on": + push: + tags: + - "v*" + workflow_dispatch: + inputs: + qwentts_ref: + description: qwentts.cpp commit, tag, or branch to build + required: true + default: 9dbe7ea26a01b30fccb117ae5e86807c1dc23d42 + hf_repo_id: + description: Hugging Face dataset repo for wheel hosting + required: true + default: andimarafioti/qwentts-cpp-python-wheels + +permissions: + contents: read + +env: + HF_WHEEL_REPO_ID: ${{ inputs.hf_repo_id || vars.HF_WHEEL_REPO_ID || 'andimarafioti/qwentts-cpp-python-wheels' }} + QWENTTS_REF: ${{ inputs.qwentts_ref || '9dbe7ea26a01b30fccb117ae5e86807c1dc23d42' }} + +jobs: + build-wheels: + name: Linux ${{ matrix.arch }} CUDA ${{ matrix.cuda_version }} HF wheel + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-22.04 + cuda_repo_distro: ubuntu2204 + cuda_repo_arch: x86_64 + cuda_version: "12.4" + cuda_package_suffix: "12-4" + cuda_home: /usr/local/cuda-12.4 + flavor: cu124 + cuda_architectures: 75-virtual;80-real;86-real;90-real + manylinux_platform: manylinux_2_35_x86_64 + build_jobs: "2" + - arch: aarch64 + runner: ubuntu-22.04-arm + cuda_repo_distro: ubuntu2204 + cuda_repo_arch: sbsa + cuda_version: "12.4" + cuda_package_suffix: "12-4" + cuda_home: /usr/local/cuda-12.4 + flavor: cu124 + cuda_architectures: 75-virtual;80-real;86-real;90-real + manylinux_platform: manylinux_2_35_aarch64 + build_jobs: "2" + - arch: x86_64 + runner: ubuntu-24.04 + cuda_repo_distro: ubuntu2404 + cuda_repo_arch: x86_64 + cuda_version: "12.8" + cuda_package_suffix: "12-8" + cuda_home: /usr/local/cuda-12.8 + flavor: cu128 + cuda_architectures: 75-virtual;80-real;86-real;90-real;120-real;120-virtual + manylinux_platform: manylinux_2_39_x86_64 + build_jobs: "2" + - arch: aarch64 + runner: ubuntu-24.04-arm + cuda_repo_distro: ubuntu2404 + cuda_repo_arch: sbsa + cuda_version: "12.8" + cuda_package_suffix: "12-8" + cuda_home: /usr/local/cuda-12.8 + flavor: cu128 + cuda_architectures: 75-virtual;80-real;86-real;90-real;120-real;120-virtual + manylinux_platform: manylinux_2_39_aarch64 + build_jobs: "2" + - arch: x86_64 + runner: ubuntu-24.04 + cuda_repo_distro: ubuntu2404 + cuda_repo_arch: x86_64 + cuda_version: "13.0" + cuda_package_suffix: "13-0" + cuda_home: /usr/local/cuda-13.0 + flavor: cu130 + cuda_architectures: 75-virtual;80-real;86-real;90-real;121-real + manylinux_platform: manylinux_2_39_x86_64 + build_jobs: "2" + - arch: aarch64 + runner: ubuntu-24.04-arm + cuda_repo_distro: ubuntu2404 + cuda_repo_arch: sbsa + cuda_version: "13.0" + cuda_package_suffix: "13-0" + cuda_home: /usr/local/cuda-13.0 + flavor: cu130 + cuda_architectures: 75-virtual;80-real;86-real;90-real;121-real + manylinux_platform: manylinux_2_39_aarch64 + build_jobs: "2" + env: + QWENTTS_CPP_BACKEND: cuda + QWENTTS_CPP_BUILD_JOBS: ${{ matrix.build_jobs }} + CUDA_ARCHITECTURES: ${{ matrix.cuda_architectures }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Free disk space + run: | + sudo rm -rf /usr/local/lib/android /opt/ghc /usr/share/dotnet + sudo apt-get clean + + - name: Install build tools + run: | + python -m pip install --upgrade pip auditwheel build wheel + sudo apt-get update + sudo apt-get install -y --no-install-recommends cmake ninja-build patchelf wget ca-certificates + + - name: Install CUDA toolkit + run: | + wget -q https://developer.download.nvidia.com/compute/cuda/repos/${{ matrix.cuda_repo_distro }}/${{ matrix.cuda_repo_arch }}/cuda-keyring_1.1-1_all.deb + sudo dpkg -i cuda-keyring_1.1-1_all.deb + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + cuda-nvcc-${{ matrix.cuda_package_suffix }} \ + cuda-cudart-dev-${{ matrix.cuda_package_suffix }} \ + libcublas-dev-${{ matrix.cuda_package_suffix }} + echo "${{ matrix.cuda_home }}/bin" >> "$GITHUB_PATH" + echo "CUDA_HOME=${{ matrix.cuda_home }}" >> "$GITHUB_ENV" + echo "CMAKE_CUDA_COMPILER=${{ matrix.cuda_home }}/bin/nvcc" >> "$GITHUB_ENV" + + - name: Fetch qwentts.cpp + run: | + git clone --recursive https://github.com/ServeurpersoCom/qwentts.cpp third_party/qwentts.cpp + git -C third_party/qwentts.cpp checkout "$QWENTTS_REF" + git -C third_party/qwentts.cpp submodule update --init --recursive + + - name: Build wheel + run: | + python scripts/set_local_version.py "${{ matrix.flavor }}" + python scripts/build_native.py \ + --source third_party/qwentts.cpp \ + --backend cuda \ + --clean \ + --cmake-arg=-G \ + --cmake-arg=Ninja \ + --cmake-arg="-DCMAKE_CUDA_ARCHITECTURES=$CUDA_ARCHITECTURES" + python -m build --wheel + mkdir -p wheelhouse + python -m auditwheel repair \ + --plat ${{ matrix.manylinux_platform }} \ + --exclude libcudart.so.12 \ + --exclude libcudart.so.13 \ + --exclude libcublas.so.12 \ + --exclude libcublas.so.13 \ + --exclude libcublasLt.so.12 \ + --exclude libcublasLt.so.13 \ + --exclude libcuda.so.1 \ + -w wheelhouse \ + dist/*.whl + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: hf-wheel-${{ matrix.flavor }}-${{ matrix.arch }} + path: wheelhouse/*.whl + + publish-hf: + name: Publish HF wheel index + needs: build-wheels + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: hf-wheel-* + path: dist + merge-multiple: true + + - name: Build wheel index + run: | + python -m pip install --upgrade pip huggingface-hub twine + python -m twine check --strict dist/*.whl + python scripts/prepare_hf_wheel_repo.py \ + --dist dist \ + --out hf-upload \ + --repo-id "$HF_WHEEL_REPO_ID" + + - name: Upload to Hugging Face + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + python scripts/upload_hf_wheels.py \ + --folder hf-upload \ + --repo-id "$HF_WHEEL_REPO_ID" \ + --commit-message "Update qwentts-cpp-python wheels for ${GITHUB_REF_NAME}" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5e3a356..bd3a88a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -26,41 +26,71 @@ jobs: fail-fast: false matrix: include: + - arch: x86_64 + runner: ubuntu-22.04 + cuda_repo_distro: ubuntu2204 + cuda_repo_arch: x86_64 + cuda_version: "12.4" + cuda_package_suffix: "12-4" + cuda_home: /usr/local/cuda-12.4 + wheel_build_tag: 1cu124 + cuda_architectures: 75-virtual;80-real;86-real;90-real + manylinux_platform: manylinux_2_35_x86_64 + build_jobs: "2" + - arch: aarch64 + runner: ubuntu-22.04-arm + cuda_repo_distro: ubuntu2204 + cuda_repo_arch: sbsa + cuda_version: "12.4" + cuda_package_suffix: "12-4" + cuda_home: /usr/local/cuda-12.4 + wheel_build_tag: 1cu124 + cuda_architectures: 75-virtual;80-real;86-real;90-real + manylinux_platform: manylinux_2_35_aarch64 + build_jobs: "2" - arch: x86_64 runner: ubuntu-24.04 + cuda_repo_distro: ubuntu2404 cuda_repo_arch: x86_64 cuda_version: "12.8" cuda_package_suffix: "12-8" cuda_home: /usr/local/cuda-12.8 wheel_build_tag: 1cu128 cuda_architectures: 75-virtual;86-real;90-real;120-real;120-virtual + manylinux_platform: manylinux_2_39_x86_64 build_jobs: "2" - arch: aarch64 runner: ubuntu-24.04-arm + cuda_repo_distro: ubuntu2404 cuda_repo_arch: sbsa cuda_version: "12.8" cuda_package_suffix: "12-8" cuda_home: /usr/local/cuda-12.8 wheel_build_tag: 1cu128 cuda_architectures: 75-virtual;86-real;90-real;120-real;120-virtual + manylinux_platform: manylinux_2_39_aarch64 build_jobs: "2" - arch: x86_64 runner: ubuntu-24.04 + cuda_repo_distro: ubuntu2404 cuda_repo_arch: x86_64 cuda_version: "13.0" cuda_package_suffix: "13-0" cuda_home: /usr/local/cuda-13.0 wheel_build_tag: 1cu130 cuda_architectures: 75-virtual;80-real;86-real;90-real;121-real + manylinux_platform: manylinux_2_39_x86_64 build_jobs: "2" - arch: aarch64 runner: ubuntu-24.04-arm + cuda_repo_distro: ubuntu2404 cuda_repo_arch: sbsa cuda_version: "13.0" cuda_package_suffix: "13-0" cuda_home: /usr/local/cuda-13.0 wheel_build_tag: 1cu130 cuda_architectures: 75-virtual;80-real;86-real;90-real;121-real + manylinux_platform: manylinux_2_39_aarch64 build_jobs: "2" env: QWENTTS_CPP_BACKEND: cuda @@ -88,7 +118,7 @@ jobs: - name: Install CUDA toolkit run: | - wget -q https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/${{ matrix.cuda_repo_arch }}/cuda-keyring_1.1-1_all.deb + wget -q https://developer.download.nvidia.com/compute/cuda/repos/${{ matrix.cuda_repo_distro }}/${{ matrix.cuda_repo_arch }}/cuda-keyring_1.1-1_all.deb sudo dpkg -i cuda-keyring_1.1-1_all.deb sudo apt-get update sudo apt-get install -y --no-install-recommends \ @@ -117,7 +147,7 @@ jobs: python -m build --wheel mkdir -p wheelhouse python -m auditwheel repair \ - --plat manylinux_2_39_${{ matrix.arch }} \ + --plat ${{ matrix.manylinux_platform }} \ --exclude libcudart.so.12 \ --exclude libcudart.so.13 \ --exclude libcublas.so.12 \ diff --git a/AGENTS.md b/AGENTS.md index dafbd32..f757fcd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,13 @@ The PR wheel workflow may still build CPU and CUDA 13 wheels as downloadable validation artifacts. Those artifacts are not uploaded to PyPI by the publish workflow. +Additional CUDA wheels can be published to Hugging Face Hub by +`.github/workflows/publish-hf-wheels.yml`. That workflow builds local-version +variants such as `0.2.0+cu124`, `0.2.0+cu128`, and `0.2.0+cu130`, prepares +static `--find-links` pages, creates the public dataset repo if needed, and +uploads the wheel index using the `HF_TOKEN` repository secret. Do not upload +those local-version CUDA variants to PyPI. + CUDA release wheels keep native cubins for sm_86, sm_90, and sm_120, plus PTX fallbacks for sm_75 and the newest supported CUDA architecture. This keeps the public CUDA 12.8 wheels under PyPI's default per-file upload limit while diff --git a/README.md b/README.md index e757b0f..8a8b8b7 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,35 @@ QWENTTS_CPP_WHEEL_BUILD_TAG=1cpu python -m build --wheel package. CPU builds are still useful for development and smoke tests, but they are not the primary release target. +## Installation + +The default PyPI package is built for CUDA 12.8: + +```bash +pip install qwentts-cpp-python +``` + +Additional CUDA wheels are published to Hugging Face Hub as local-version +variants. Use them when the PyPI CUDA 12.8 wheel does not match the runtime or +GPU target, for example DGX Spark / GB10 with CUDA 13: + +```bash +pip install "qwentts-cpp-python==0.2.0+cu124" \ + -f https://huggingface.co/datasets/andimarafioti/qwentts-cpp-python-wheels/resolve/main/whl/cu124.html + +pip install "qwentts-cpp-python==0.2.0+cu128" \ + -f https://huggingface.co/datasets/andimarafioti/qwentts-cpp-python-wheels/resolve/main/whl/cu128.html + +pip install "qwentts-cpp-python==0.2.0+cu130" \ + -f https://huggingface.co/datasets/andimarafioti/qwentts-cpp-python-wheels/resolve/main/whl/cu130.html +``` + +Hugging Face raw file hosting does not expose a PyTorch-style package directory +index, so these commands use pip's `--find-links` mode. Dependencies still +resolve from PyPI normally. The wheels do not bundle CUDA runtime or cuBLAS +libraries; use a base image or system installation that provides the matching +CUDA runtime. + The CI wheel build defaults to qwentts.cpp `9dbe7ea26a01b30fccb117ae5e86807c1dc23d42`, which includes the scheduler resets and ABI v2 cached voice-reference fields. CPU and CUDA both stay on the diff --git a/scripts/prepare_hf_wheel_repo.py b/scripts/prepare_hf_wheel_repo.py new file mode 100644 index 0000000..03dd778 --- /dev/null +++ b/scripts/prepare_hf_wheel_repo.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +import argparse +import html +import re +import shutil +from pathlib import Path + + +WHEEL_RE = re.compile( + r"^qwentts_cpp_python-(?P[^-]+)-py3-none-(?P.+)\.whl$" +) + + +def _flavor_from_wheel(path: Path) -> str: + match = WHEEL_RE.match(path.name) + if not match: + raise ValueError(f"Unexpected wheel filename: {path.name}") + version = match.group("version") + if "+" not in version: + raise ValueError(f"Wheel does not include a local version flavor: {path.name}") + return version.split("+", 1)[1] + + +def _version_from_wheel(path: Path) -> str: + match = WHEEL_RE.match(path.name) + if not match: + raise ValueError(f"Unexpected wheel filename: {path.name}") + return match.group("version") + + +def _write_links_page(path: Path, title: str, links: list[tuple[str, str]]) -> None: + items = "\n".join( + f'
  • {html.escape(label)}
  • ' for href, label in links + ) + path.write_text( + "\n".join( + [ + "", + "", + " ", + ' ', + f" {html.escape(title)}", + " ", + " ", + f"

    {html.escape(title)}

    ", + " ", + " ", + "", + "", + ] + ) + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Build static Hugging Face wheel index pages.") + parser.add_argument("--dist", type=Path, required=True, help="Directory containing built wheels") + parser.add_argument("--out", type=Path, required=True, help="Output directory to upload") + parser.add_argument("--repo-id", required=True, help="HF dataset repo id used in generated README") + args = parser.parse_args() + + wheels = sorted(args.dist.rglob("qwentts_cpp_python-*.whl")) + if not wheels: + raise SystemExit(f"No wheels found under {args.dist}") + + if args.out.exists(): + shutil.rmtree(args.out) + wheel_root = args.out / "whl" + wheel_root.mkdir(parents=True) + + by_flavor: dict[str, list[Path]] = {} + versions_by_flavor: dict[str, str] = {} + for wheel in wheels: + flavor = _flavor_from_wheel(wheel) + version = _version_from_wheel(wheel) + previous = versions_by_flavor.setdefault(flavor, version) + if previous != version: + raise ValueError(f"Mixed versions for {flavor}: {previous} and {version}") + target_dir = wheel_root / flavor + target_dir.mkdir(parents=True, exist_ok=True) + target = target_dir / wheel.name + shutil.copy2(wheel, target) + by_flavor.setdefault(flavor, []).append(target) + + flavor_links: list[tuple[str, str]] = [] + for flavor in sorted(by_flavor): + files = sorted(by_flavor[flavor]) + _write_links_page( + wheel_root / f"{flavor}.html", + f"qwentts-cpp-python {flavor} wheels", + [(f"{flavor}/{wheel.name}", wheel.name) for wheel in files], + ) + flavor_links.append((f"{flavor}.html", flavor)) + + _write_links_page(wheel_root / "index.html", "qwentts-cpp-python wheels", flavor_links) + + args.out.joinpath("README.md").write_text( + "\n".join( + [ + "---", + "license: mit", + "---", + "", + "# qwentts-cpp-python wheels", + "", + "Optional CUDA wheel variants for `qwentts-cpp-python`.", + "", + "The default PyPI package is CUDA 12.8:", + "", + "```bash", + "pip install qwentts-cpp-python", + "```", + "", + "Install a CUDA-specific wheel from this repository with `--find-links`:", + "", + "```bash", + *[ + f'pip install "qwentts-cpp-python=={versions_by_flavor[flavor]}" -f https://huggingface.co/datasets/{args.repo_id}/resolve/main/whl/{flavor}.html' + for flavor in sorted(versions_by_flavor) + ], + "```", + "", + "The wheels do not bundle CUDA runtime or cuBLAS libraries. Use a base image or", + "system installation that provides the matching CUDA runtime.", + "", + ] + ) + ) + print(f"Prepared {sum(len(v) for v in by_flavor.values())} wheels for {', '.join(sorted(by_flavor))}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/set_local_version.py b/scripts/set_local_version.py new file mode 100644 index 0000000..53ffc60 --- /dev/null +++ b/scripts/set_local_version.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import argparse +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PYPROJECT = ROOT / "pyproject.toml" +INIT = ROOT / "src" / "qwentts_cpp" / "__init__.py" + + +def _replace(pattern: str, repl: str, text: str, path: Path) -> str: + updated, count = re.subn(pattern, repl, text, count=1, flags=re.MULTILINE) + if count != 1: + raise SystemExit(f"Could not update version in {path}") + return updated + + +def main() -> int: + parser = argparse.ArgumentParser(description="Append a PEP 440 local version suffix for wheel builds.") + parser.add_argument("suffix", help="Local version suffix without '+', for example cu130") + args = parser.parse_args() + + suffix = args.suffix.strip().lstrip("+") + if not re.fullmatch(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", suffix): + raise SystemExit(f"Invalid local version suffix: {args.suffix!r}") + + pyproject_text = PYPROJECT.read_text() + match = re.search(r'^version = "([^"]+)"$', pyproject_text, flags=re.MULTILINE) + if not match: + raise SystemExit(f"Could not find project version in {PYPROJECT}") + base_version = match.group(1).split("+", 1)[0] + version = f"{base_version}+{suffix}" + + PYPROJECT.write_text( + _replace(r'^version = "[^"]+"$', f'version = "{version}"', pyproject_text, PYPROJECT) + ) + INIT.write_text( + _replace(r'^__version__ = "[^"]+"$', f'__version__ = "{version}"', INIT.read_text(), INIT) + ) + print(version) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/upload_hf_wheels.py b/scripts/upload_hf_wheels.py new file mode 100644 index 0000000..03d458f --- /dev/null +++ b/scripts/upload_hf_wheels.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import argparse +import os +from pathlib import Path + +from huggingface_hub import HfApi + + +def main() -> int: + parser = argparse.ArgumentParser(description="Upload prepared wheels to a Hugging Face dataset repo.") + parser.add_argument("--folder", type=Path, required=True) + parser.add_argument("--repo-id", required=True) + parser.add_argument("--revision", default="main") + parser.add_argument("--commit-message", default="Update qwentts-cpp-python wheels") + args = parser.parse_args() + + token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN") + if not token: + raise SystemExit("HF_TOKEN or HUGGINGFACE_HUB_TOKEN is required") + + api = HfApi(token=token) + api.create_repo(args.repo_id, repo_type="dataset", private=False, exist_ok=True) + api.upload_folder( + repo_id=args.repo_id, + repo_type="dataset", + revision=args.revision, + folder_path=str(args.folder), + path_in_repo=".", + commit_message=args.commit_message, + ) + print(f"Uploaded {args.folder} to dataset {args.repo_id}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())