Use cibuildwheel supported prebuilt CUDA images #656
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries | |
| # https://github.com/pypa/cibuildwheel/blob/main/examples/github-deploy.yml | |
| name: Build and upload to PyPI | |
| on: | |
| workflow_dispatch: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| release: | |
| types: [published] | |
| jobs: | |
| build_wheels: | |
| name: Build wheels for ${{ matrix.os }}-cu${{ matrix.cuda_major }} | |
| runs-on: ${{ matrix.runs-on }} | |
| strategy: | |
| matrix: | |
| include: | |
| # CUDA 12 wheels (compiled with 12.2 for runtime 12.2+ compatibility) | |
| - os: linux-intel | |
| cuda_major: "12" | |
| cuda_minor: "2" | |
| runs-on: ubuntu-latest | |
| cibw_image: "quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda12_2:latest" | |
| cuda_archs: "75-real;80-real;86-real;89-real;90" | |
| - os: linux-arm | |
| cuda_major: "12" | |
| cuda_minor: "2" | |
| runs-on: ubuntu-24.04-arm | |
| cibw_image: "quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda12_2:latest" | |
| cuda_archs: "75-real;80-real;86-real;89-real;90" | |
| # CUDA 13 wheels (native Blackwell support) | |
| - os: linux-intel | |
| cuda_major: "13" | |
| cuda_minor: "0" | |
| runs-on: ubuntu-latest | |
| cibw_image: "quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda13_0:latest" | |
| cuda_archs: "75-real;80-real;86-real;89-real;90-real;100-real;120" | |
| - os: linux-arm | |
| cuda_major: "13" | |
| cuda_minor: "0" | |
| runs-on: ubuntu-24.04-arm | |
| cibw_image: "quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda13_0:latest" | |
| cuda_archs: "75-real;80-real;86-real;89-real;90-real;100-real;120" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get version from setuptools-scm | |
| id: version | |
| run: | | |
| pip install setuptools-scm | |
| VERSION=$(python -m setuptools_scm) | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Detected version: $VERSION" | |
| - name: Set package name, extras, and CUDA archs for CUDA ${{ matrix.cuda_major }} | |
| run: | | |
| python3 - ${{ matrix.cuda_major }} "${{ matrix.cuda_archs }}" <<'SCRIPT' | |
| import sys, pathlib | |
| cuda = sys.argv[1] | |
| cuda_archs = sys.argv[2] | |
| other = "13" if cuda == "12" else "12" | |
| path = pathlib.Path("pyproject.toml") | |
| text = path.read_text() | |
| def remove_toml_array(text, key): | |
| lines = text.splitlines(keepends=True) | |
| out = [] | |
| i = 0 | |
| while i < len(lines): | |
| if lines[i].startswith(f"{key} = ["): | |
| depth = lines[i].count("[") - lines[i].count("]") | |
| i += 1 | |
| while i < len(lines) and depth > 0: | |
| depth += lines[i].count("[") - lines[i].count("]") | |
| i += 1 | |
| continue | |
| out.append(lines[i]) | |
| i += 1 | |
| return "".join(out) | |
| # Rename package | |
| text = text.replace( | |
| 'name = "rapids-singlecell"', | |
| f'name = "rapids-singlecell-cu{cuda}"', | |
| ) | |
| # Rename matching extra to "rapids", remove the other | |
| text = text.replace(f'rapids-cu{cuda} = [', 'rapids = [') | |
| text = remove_toml_array(text, f"rapids-cu{other}") | |
| # CMake links CUDA extensions against librmm. | |
| # Build against the oldest supported RMM ABI so the wheel can run | |
| # with any supported runtime RMM version. | |
| build_system_text, project_text = text.split("[project]", 1) | |
| for dep in ( | |
| f' "librmm-cu{other}>=25.12",\n', | |
| f' "librmm-cu{other}>=25.12,<25.13",\n', | |
| f' "rmm-cu{other}>=25.12",\n', | |
| f' "rmm-cu{other}>=25.12,<25.13",\n', | |
| f' "librmm-cu{cuda}>=25.12",\n', | |
| f' "librmm-cu{cuda}>=25.12,<25.13",\n', | |
| f' "rmm-cu{cuda}>=25.12",\n', | |
| f' "rmm-cu{cuda}>=25.12,<25.13",\n', | |
| ): | |
| build_system_text = build_system_text.replace(dep, "") | |
| rmm_build_req = f' "librmm-cu{cuda}>=25.12,<25.13",\n' | |
| if f'"librmm-cu{cuda}>=25.12"' not in build_system_text: | |
| build_system_text = build_system_text.replace( | |
| ']\nbuild-backend = "scikit_build_core.build"', | |
| f'{rmm_build_req}]\nbuild-backend = "scikit_build_core.build"', | |
| 1, | |
| ) | |
| text = build_system_text + "[project]" + project_text | |
| # Set CUDA architectures (replace "native" with CI target archs) | |
| text = text.replace( | |
| 'CMAKE_CUDA_ARCHITECTURES = "native"', | |
| f'CMAKE_CUDA_ARCHITECTURES = "{cuda_archs}"', | |
| ) | |
| path.write_text(text) | |
| # Verify | |
| for line in path.read_text().splitlines(): | |
| if "name" in line or "rapids" in line.lower() or "CUDA_ARCH" in line: | |
| print(line) | |
| SCRIPT | |
| - name: Sanity check pyproject.toml | |
| run: | | |
| python3 -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb'))" | |
| grep -E "name|rapids|CUDA_ARCH" pyproject.toml | |
| - name: Build wheels | |
| uses: pypa/cibuildwheel@v3.4.1 | |
| env: | |
| SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.version.outputs.version }} | |
| CIBW_BUILD: 'cp312-*' | |
| CIBW_SKIP: '*-musllinux*' | |
| CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.os == 'linux-intel' && matrix.cibw_image || '' }} | |
| CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.os == 'linux-arm' && matrix.cibw_image || '' }} | |
| CIBW_ENVIRONMENT_PASS_LINUX: SETUPTOOLS_SCM_PRETEND_VERSION | |
| CIBW_ENVIRONMENT: > | |
| CUDA_PATH=/usr/local/cuda | |
| LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH | |
| PATH=/usr/local/cuda/bin:$PATH | |
| CIBW_BEFORE_ALL: > | |
| dnf install -y --setopt=obsoletes=0 | |
| libcusolver-devel-${{ matrix.cuda_major }}-${{ matrix.cuda_minor }} | |
| libcusparse-devel-${{ matrix.cuda_major }}-${{ matrix.cuda_minor }} | |
| libnvjitlink-${{ matrix.cuda_major }}-${{ matrix.cuda_minor }} | |
| CIBW_BEFORE_BUILD: > | |
| rm -f build/.librmm_dir && | |
| mkdir -p build && | |
| python -m pip install -U pip | |
| scikit-build-core cmake ninja nanobind | |
| "librmm-cu${{ matrix.cuda_major }}>=25.12,<25.13" && | |
| RMM_ROOT=$(python -c "import librmm; print(librmm.__path__[0])") && | |
| LOG_ROOT=$(python -c "import rapids_logger; print(rapids_logger.__path__[0])") && | |
| echo "[rsc-build] librmm=$RMM_ROOT" && | |
| echo "[rsc-build] rapids_logger=$LOG_ROOT" && | |
| ln -sf "$RMM_ROOT/lib64/librmm.so" /usr/local/lib/librmm.so && | |
| ln -sf "$LOG_ROOT/lib64/librapids_logger.so" /usr/local/lib/librapids_logger.so && | |
| ldconfig && | |
| python -c "import librmm; print(librmm.__path__[0])" > build/.librmm_dir && | |
| echo "[rsc-build] marker=$(cat build/.librmm_dir)" | |
| CIBW_TEST_SKIP: "*" | |
| CIBW_TEST_COMMAND: "" | |
| # Exclude CUDA/RAPIDS runtime libs provided by dependency wheels. | |
| # Use SONAME globs so CUDA 12/13 suffix changes do not bundle them. | |
| CIBW_REPAIR_WHEEL_COMMAND: "auditwheel repair --exclude 'libcublas.so.*' --exclude 'libcublasLt.so.*' --exclude 'libcudart.so.*' --exclude 'libcusolver.so.*' --exclude 'libcusparse.so.*' --exclude 'libnvJitLink.so.*' --exclude librmm.so --exclude librapids_logger.so -w {dest_dir} {wheel}" | |
| CIBW_BUILD_VERBOSITY: "1" | |
| - name: Check RMM ABI compatibility | |
| run: | | |
| python3 - <<'SCRIPT' | |
| import pathlib | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import zipfile | |
| nm = shutil.which("nm") | |
| if nm is None: | |
| raise SystemExit("nm is required for the RMM ABI compatibility check") | |
| failures = [] | |
| for wheel in sorted(pathlib.Path("wheelhouse").glob("*.whl")): | |
| with tempfile.TemporaryDirectory() as tmp: | |
| tmp_path = pathlib.Path(tmp) | |
| with zipfile.ZipFile(wheel) as zf: | |
| for name in zf.namelist(): | |
| if name.startswith("rapids_singlecell/_cuda/") and name.endswith(".so"): | |
| zf.extract(name, tmp_path) | |
| for ext in sorted((tmp_path / "rapids_singlecell" / "_cuda").glob("*.so")): | |
| result = subprocess.run( | |
| [nm, "-D", str(ext)], | |
| check=True, | |
| capture_output=True, | |
| text=True, | |
| ) | |
| for line in result.stdout.splitlines(): | |
| if " U _ZN3rmm6detail" in line: | |
| failures.append(f"{wheel.name}:{ext.name}: {line.strip()}") | |
| if failures: | |
| print( | |
| "Wheel links against private RMM detail symbols; build against the " | |
| "oldest supported RMM ABI instead.", | |
| file=sys.stderr, | |
| ) | |
| print("\n".join(failures), file=sys.stderr) | |
| raise SystemExit(1) | |
| SCRIPT | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: cibw-wheels-${{ matrix.os }}-cu${{ matrix.cuda_major }}-${{ strategy.job-index }} | |
| path: ./wheelhouse/*.whl | |
| build_sdist: | |
| name: Build source distribution | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Build sdist | |
| run: pipx run build --sdist | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: cibw-sdist | |
| path: dist/*.tar.gz | |
| upload_wheels: | |
| needs: [build_wheels] | |
| runs-on: ubuntu-latest | |
| environment: publish-cu${{ matrix.cuda_major }} | |
| permissions: | |
| id-token: write | |
| if: github.event_name == 'release' && github.event.action == 'published' | |
| strategy: | |
| matrix: | |
| cuda_major: ["12", "13"] | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| pattern: cibw-wheels-*-cu${{ matrix.cuda_major }}-* | |
| path: dist | |
| merge-multiple: true | |
| - uses: pypa/gh-action-pypi-publish@release/v1 | |
| upload_sdist: | |
| needs: [build_sdist] | |
| runs-on: ubuntu-latest | |
| environment: publish | |
| permissions: | |
| id-token: write | |
| if: github.event_name == 'release' && github.event.action == 'published' | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: cibw-sdist | |
| path: dist | |
| - uses: pypa/gh-action-pypi-publish@release/v1 |