Skip to content

Use cibuildwheel supported prebuilt CUDA images #656

Use cibuildwheel supported prebuilt CUDA images

Use cibuildwheel supported prebuilt CUDA images #656

Workflow file for this run

# 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