From d772dfe6cf74ff07d561271e3f3335cbb832a992 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 14 Aug 2026 16:08:49 -0500 Subject: [PATCH 1/8] Build cuopt as an abi3 wheel using the CPython Limited API Build one cuopt wheel per (CUDA major, arch) instead of one per Python version, by targeting the CPython Limited API. This drops the cuopt wheel build from 8 CI jobs to 2 and removes the need for a new build when a Python version is added. - python/cuopt: set wheel.py-api = cp311, matching requires-python - bump cython to >=3.2.2,<3.3.0a0 for its Limited API support - name the wheel artifact with 'rapids-artifact-name --stable' (abi3) on both the producing and consuming sides - filter the wheel-build-cuopt matrix to the minimum supported Python Wheel tests still run against the full Python matrix, now all installing the single abi3 wheel. Closes #1725 Signed-off-by: Ramakrishna Prabhu --- .github/workflows/build.yaml | 2 ++ .github/workflows/pr.yaml | 4 ++++ ci/build_wheel.sh | 21 +++++++++++++++++++ ci/build_wheel_cuopt.sh | 9 ++++++-- ci/test_self_hosted_service.sh | 2 +- ci/test_wheel_cuopt.sh | 2 +- ci/test_wheel_cuopt_server.sh | 2 +- .../all_cuda-129_arch-aarch64.yaml | 2 +- .../all_cuda-129_arch-x86_64.yaml | 2 +- .../all_cuda-133_arch-aarch64.yaml | 2 +- .../all_cuda-133_arch-x86_64.yaml | 2 +- dependencies.yaml | 2 +- python/cuopt/pyproject.toml | 3 ++- 13 files changed, 44 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3f82880437..062afd6f98 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -157,6 +157,8 @@ jobs: script: ci/build_wheel_cuopt.sh package-name: cuopt package-type: python + # abi3 wheel: build one per CUDA x ARCH, using the minimum supported Python + matrix_filter: group_by({CUDA_VER, ARCH}) | map(min_by(.PY_VER | split(".") | map(tonumber))) wheel-publish-cuopt: needs: wheel-build-cuopt permissions: diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index d54afeebd4..3881da5325 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -48,6 +48,7 @@ jobs: runs-on: ubuntu-latest outputs: libcuopt_filter: ${{ steps.set-filters.outputs.libcuopt_filter }} + cuopt_filter: ${{ steps.set-filters.outputs.cuopt_filter }} cuopt_server_filter: ${{ steps.set-filters.outputs.cuopt_server_filter }} cuopt_server_test_filter: ${{ steps.set-filters.outputs.cuopt_server_test_filter }} cuopt_sh_client_filter: ${{ steps.set-filters.outputs.cuopt_sh_client_filter }} @@ -56,6 +57,7 @@ jobs: id: set-filters run: | echo "libcuopt_filter=group_by([.ARCH, (.CUDA_VER|split(\".\")|map(tonumber)|.[0])]) | map(max_by(.PY_VER|split(\".\")|map(tonumber)))" >> $GITHUB_OUTPUT + echo "cuopt_filter=group_by({CUDA_VER, ARCH}) | map(min_by(.PY_VER | split(\".\") | map(tonumber)))" >> $GITHUB_OUTPUT echo "cuopt_server_filter=map(select(.ARCH == \"amd64\")) | group_by(.CUDA_VER|split(\".\")|map(tonumber)|.[0]) | map(max_by([(.PY_VER|split(\".\")|map(tonumber)), (.CUDA_VER|split(\".\")|map(tonumber))]))" >> $GITHUB_OUTPUT echo "cuopt_server_test_filter=map(select(.ARCH == \"amd64\")) | group_by(.CUDA_VER | split(\".\") | map(tonumber) | .[0]) | map(max_by([(.PY_VER | split(\".\") | map(tonumber)), (.CUDA_VER | split(\".\") | map(tonumber))]))" >> $GITHUB_OUTPUT echo "cuopt_sh_client_filter=[map(select(.ARCH == \"amd64\")) | min_by((.PY_VER | split(\".\") | map(tonumber)), (.CUDA_VER | split(\".\") | map(-tonumber)))]" >> $GITHUB_OUTPUT @@ -479,6 +481,8 @@ jobs: script: ci/build_wheel_cuopt.sh package-name: cuopt package-type: python + # abi3 wheel: build one per CUDA x ARCH, using the minimum supported Python + matrix_filter: ${{ needs.compute-matrix-filters.outputs.cuopt_filter }} wheel-tests-cuopt: needs: [wheel-build-cuopt, wheel-build-cuopt-sh-client, changed-files] permissions: diff --git a/ci/build_wheel.sh b/ci/build_wheel.sh index 8cac0e7714..92ec1cc2f2 100755 --- a/ci/build_wheel.sh +++ b/ci/build_wheel.sh @@ -7,6 +7,22 @@ set -euo pipefail package_name=$1 package_dir=$2 +shift 2 + +# Parse optional flags +stable_abi=false +while [[ $# -gt 0 ]]; do + case "$1" in + --stable) + stable_abi=true + shift + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done source rapids-configure-sccache source rapids-datetime-string @@ -29,6 +45,11 @@ RAPIDS_PIP_WHEEL_ARGS=( --disable-pip-version-check ) +# Add py-api setting for stable ABI builds +if [[ "${stable_abi}" == "true" ]] && [[ -n "${RAPIDS_PY_API:-}" ]]; then + RAPIDS_PIP_WHEEL_ARGS+=(--config-settings="skbuild.wheel.py-api=${RAPIDS_PY_API}") +fi + # Only use --build-constraint when build isolation is enabled. # # Passing '--build-constraint' and '--no-build-isolation` together results in an error from 'pip', diff --git a/ci/build_wheel_cuopt.sh b/ci/build_wheel_cuopt.sh index f624b27705..a41f8439af 100755 --- a/ci/build_wheel_cuopt.sh +++ b/ci/build_wheel_cuopt.sh @@ -45,12 +45,17 @@ EXCLUDE_ARGS=( --exclude "librmm.so" ) -ci/build_wheel.sh cuopt ${package_dir} +# TODO: move this variable into `ci-wheel` +# Format Python limited API version string +RAPIDS_PY_API="cp${RAPIDS_PY_VERSION//./}" +export RAPIDS_PY_API + +ci/build_wheel.sh cuopt ${package_dir} --stable # repair wheels and write to the location that artifact-uploading code expects to find them python -m auditwheel repair "${EXCLUDE_ARGS[@]}" -w ${RAPIDS_WHEEL_BLD_OUTPUT_DIR} ${package_dir}/dist/* ci/validate_wheel.sh "${package_dir}" "${RAPIDS_WHEEL_BLD_OUTPUT_DIR}" -RAPIDS_PACKAGE_NAME="$(rapids-artifact-name wheel_python cuopt cuopt --py "$RAPIDS_PY_VERSION" --cuda "$RAPIDS_CUDA_VERSION")" +RAPIDS_PACKAGE_NAME="$(rapids-artifact-name wheel_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")" export RAPIDS_PACKAGE_NAME diff --git a/ci/test_self_hosted_service.sh b/ci/test_self_hosted_service.sh index 63776a4f6c..07774bf200 100755 --- a/ci/test_self_hosted_service.sh +++ b/ci/test_self_hosted_service.sh @@ -9,7 +9,7 @@ source rapids-init-pip # Download the cuopt built in the previous step LIBCUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") -CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt cuopt --py "$RAPIDS_PY_VERSION" --cuda "$RAPIDS_CUDA_VERSION")") +CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")") CUOPT_SERVER_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt-server cuopt --pure --arch any --cuda "$RAPIDS_CUDA_VERSION")") # generate constraints (possibly pinning to oldest support versions of dependencies) diff --git a/ci/test_wheel_cuopt.sh b/ci/test_wheel_cuopt.sh index 12c8d2396b..86c1cbc565 100755 --- a/ci/test_wheel_cuopt.sh +++ b/ci/test_wheel_cuopt.sh @@ -17,7 +17,7 @@ bash "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/utils/install_openssl3_runtim # Download the packages built in the previous step RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen "${RAPIDS_CUDA_VERSION}")" LIBCUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") -CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt cuopt --py "$RAPIDS_PY_VERSION" --cuda "$RAPIDS_CUDA_VERSION")") +CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")") CUOPT_SH_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt-sh-client cuopt --pure --arch any)") # update pip constraints.txt to ensure all future 'pip install' (including those in ci/thirdparty-testing) diff --git a/ci/test_wheel_cuopt_server.sh b/ci/test_wheel_cuopt_server.sh index df1749a3b5..0f6d6fd19e 100755 --- a/ci/test_wheel_cuopt_server.sh +++ b/ci/test_wheel_cuopt_server.sh @@ -14,7 +14,7 @@ bash "$(dirname "$(realpath "${BASH_SOURCE[0]}")")/utils/install_openssl3_runtim # Download the packages built in the previous step LIBCUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_cpp libcuopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") -CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt cuopt --py "$RAPIDS_PY_VERSION" --cuda "$RAPIDS_CUDA_VERSION")") +CUOPT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")") CUOPT_SERVER_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt-server cuopt --pure --arch any --cuda "$RAPIDS_CUDA_VERSION")") CUOPT_SH_CLIENT_WHEELHOUSE=$(rapids-download-from-github "$(rapids-artifact-name wheel_python cuopt-sh-client cuopt --pure --arch any)") diff --git a/conda/environments/all_cuda-129_arch-aarch64.yaml b/conda/environments/all_cuda-129_arch-aarch64.yaml index 64a6ff58db..5f0be5141b 100644 --- a/conda/environments/all_cuda-129_arch-aarch64.yaml +++ b/conda/environments/all_cuda-129_arch-aarch64.yaml @@ -21,7 +21,7 @@ dependencies: - cudf==26.10.*,>=0.0.0a0 - cupy>=14.0.1,!=14.1.0 - cxx-compiler -- cython>=3.0.3 +- cython>=3.2.2,<3.3.0a0 - docutils>=0.21 - doxygen=1.9.1 - fastapi diff --git a/conda/environments/all_cuda-129_arch-x86_64.yaml b/conda/environments/all_cuda-129_arch-x86_64.yaml index e2d2a3b77a..cdd4c8d0c1 100644 --- a/conda/environments/all_cuda-129_arch-x86_64.yaml +++ b/conda/environments/all_cuda-129_arch-x86_64.yaml @@ -21,7 +21,7 @@ dependencies: - cudf==26.10.*,>=0.0.0a0 - cupy>=14.0.1,!=14.1.0 - cxx-compiler -- cython>=3.0.3 +- cython>=3.2.2,<3.3.0a0 - docutils>=0.21 - doxygen=1.9.1 - fastapi diff --git a/conda/environments/all_cuda-133_arch-aarch64.yaml b/conda/environments/all_cuda-133_arch-aarch64.yaml index 34e60cfb0b..0628e93e65 100644 --- a/conda/environments/all_cuda-133_arch-aarch64.yaml +++ b/conda/environments/all_cuda-133_arch-aarch64.yaml @@ -21,7 +21,7 @@ dependencies: - cudf==26.10.*,>=0.0.0a0 - cupy>=14.0.1,!=14.1.0 - cxx-compiler -- cython>=3.0.3 +- cython>=3.2.2,<3.3.0a0 - docutils>=0.21 - doxygen=1.9.1 - fastapi diff --git a/conda/environments/all_cuda-133_arch-x86_64.yaml b/conda/environments/all_cuda-133_arch-x86_64.yaml index 8f6c59293f..c05ca31248 100644 --- a/conda/environments/all_cuda-133_arch-x86_64.yaml +++ b/conda/environments/all_cuda-133_arch-x86_64.yaml @@ -21,7 +21,7 @@ dependencies: - cudf==26.10.*,>=0.0.0a0 - cupy>=14.0.1,!=14.1.0 - cxx-compiler -- cython>=3.0.3 +- cython>=3.2.2,<3.3.0a0 - docutils>=0.21 - doxygen=1.9.1 - fastapi diff --git a/dependencies.yaml b/dependencies.yaml index c9eb8d3239..ca87d3d3b8 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -303,7 +303,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - cython>=3.0.3 + - cython>=3.2.2,<3.3.0a0 test_python_common: common: - output_types: [conda, requirements, pyproject] diff --git a/python/cuopt/pyproject.toml b/python/cuopt/pyproject.toml index 8aa6d18398..929d066d0e 100644 --- a/python/cuopt/pyproject.toml +++ b/python/cuopt/pyproject.toml @@ -89,6 +89,7 @@ minimum-version = "build-system.requires" ninja.make-fallback = false sdist.reproducible = true wheel.packages = ["cuopt"] +wheel.py-api = "cp311" # overridden in CI builds by arguments from `ci/build_wheel_cuopt.sh` [tool.scikit-build.metadata.version] provider = "scikit_build_core.metadata.regex" @@ -102,7 +103,7 @@ matrix-entry = "cuda_suffixed=true;use_cuda_wheels=true" requires = [ "cmake>=4.0", "cupy-cuda13x[ctk]>=14.0.1,!=14.1.0", - "cython>=3.0.3", + "cython>=3.2.2,<3.3.0a0", "libcuopt==26.10.*,>=0.0.0a0", "ninja", "pylibraft==26.10.*,>=0.0.0a0", From 3c6ed5d95726e6140124b1af0347b78dc5b49eb0 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Fri, 14 Aug 2026 16:08:54 -0500 Subject: [PATCH 2/8] cuopt-skill-evolution: mirror rmm/cudf for shared RAPIDS build conventions Surfaced while adding abi3 wheel support: the first attempt invented a generic pip-arg passthrough instead of the '--stable' flag and 'RAPIDS_PY_API' env var that rmm and cudf already use, and would have missed 'rapids-artifact-name --stable' entirely. Signed-off-by: Ramakrishna Prabhu --- skills/cuopt-developer/references/contributing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/skills/cuopt-developer/references/contributing.md b/skills/cuopt-developer/references/contributing.md index 8ae36c831d..c717a88fb1 100644 --- a/skills/cuopt-developer/references/contributing.md +++ b/skills/cuopt-developer/references/contributing.md @@ -109,6 +109,7 @@ Follow YAGNI strictly here — flags, fallbacks, env-var overrides, and config k A few non-YAGNI points worth keeping in mind: - Prefer extending an existing script over adding a new one. +- For build/CI conventions shared across RAPIDS (wheel packaging, artifact naming, matrix filters), read the reference implementation in `rapidsai/rmm` or `rapidsai/cudf` on `main` before writing anything. cuOpt's `ci/` scripts and shared-workflow inputs are near-copies of theirs, and gha-tools expects exact conventions — an equivalent-but-different local invention silently breaks the download side, which looks for the name the build side wrote. - Validate inputs at the top, before any expensive work. - One shell command per line over chained `&&`; no comments that restate the next line. - Keep informational CI jobs (reporting, dashboards, comment posting) out of any required-checks list. From ea266e55428750589ed119f9ad5f763b61950a3f Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Mon, 17 Aug 2026 16:58:38 -0500 Subject: [PATCH 3/8] docs: note the Limited API constraint on Cython code The cuopt wheel is now built as abi3, so CPython APIs outside the Limited API fail to compile. Point contributors at the constraint next to the existing Cython build note. Signed-off-by: Ramakrishna Prabhu --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53ff2ce9dc..7454489f75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -215,6 +215,9 @@ To build all libraries and tests, simply run ``` - **Note**: if Cython files (`*.pyx` or `*.pxd`) have changed, the Python build must be rerun. +- **Note**: the `cuopt` wheel is built against the CPython Limited API (abi3), so Cython code must + avoid CPython APIs outside it. Using one surfaces as a compile error under `-DPy_LIMITED_API`, + not at runtime. To run the C++ tests, run From a409e23f4caed86a0111d0abc7b5c5d626e5906d Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 18 Aug 2026 09:46:52 -0500 Subject: [PATCH 4/8] Address review feedback - add abi3audit to ci/validate_wheel.sh, matching cudf/rmm. The '*abi*' name filter skips the py3-none wheels, which abi3audit rejects for having DSOs with no ABI tag. - group the cuopt wheel matrix by CUDA major rather than full CUDA version. Artifact names encode only the major, so two minors in one major would have produced two jobs racing on the same artifact name. No change to the current matrix. - skills: cudf now lives in the NVIDIA org - reword the Limited API note in CONTRIBUTING.md Signed-off-by: Ramakrishna Prabhu --- .github/workflows/build.yaml | 6 ++++-- .github/workflows/pr.yaml | 4 ++-- CONTRIBUTING.md | 4 ++-- ci/validate_wheel.sh | 10 ++++++++++ skills/cuopt-developer/references/contributing.md | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 062afd6f98..a1f09d5288 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -157,8 +157,10 @@ jobs: script: ci/build_wheel_cuopt.sh package-name: cuopt package-type: python - # abi3 wheel: build one per CUDA x ARCH, using the minimum supported Python - matrix_filter: group_by({CUDA_VER, ARCH}) | map(min_by(.PY_VER | split(".") | map(tonumber))) + # abi3 wheel: build one per ARCH + CUDA major, using the minimum supported Python. + # Grouping on the CUDA major (not the full version) keeps one job per published + # artifact name, which only encodes the major. + matrix_filter: group_by([.ARCH, (.CUDA_VER|split(".")|map(tonumber)|.[0])]) | map(min_by((.PY_VER|split(".")|map(tonumber)), (.CUDA_VER|split(".")|map(-tonumber)))) wheel-publish-cuopt: needs: wheel-build-cuopt permissions: diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 3881da5325..342682ae06 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -57,7 +57,7 @@ jobs: id: set-filters run: | echo "libcuopt_filter=group_by([.ARCH, (.CUDA_VER|split(\".\")|map(tonumber)|.[0])]) | map(max_by(.PY_VER|split(\".\")|map(tonumber)))" >> $GITHUB_OUTPUT - echo "cuopt_filter=group_by({CUDA_VER, ARCH}) | map(min_by(.PY_VER | split(\".\") | map(tonumber)))" >> $GITHUB_OUTPUT + echo "cuopt_filter=group_by([.ARCH, (.CUDA_VER|split(\".\")|map(tonumber)|.[0])]) | map(min_by((.PY_VER|split(\".\")|map(tonumber)), (.CUDA_VER|split(\".\")|map(-tonumber))))" >> $GITHUB_OUTPUT echo "cuopt_server_filter=map(select(.ARCH == \"amd64\")) | group_by(.CUDA_VER|split(\".\")|map(tonumber)|.[0]) | map(max_by([(.PY_VER|split(\".\")|map(tonumber)), (.CUDA_VER|split(\".\")|map(tonumber))]))" >> $GITHUB_OUTPUT echo "cuopt_server_test_filter=map(select(.ARCH == \"amd64\")) | group_by(.CUDA_VER | split(\".\") | map(tonumber) | .[0]) | map(max_by([(.PY_VER | split(\".\") | map(tonumber)), (.CUDA_VER | split(\".\") | map(tonumber))]))" >> $GITHUB_OUTPUT echo "cuopt_sh_client_filter=[map(select(.ARCH == \"amd64\")) | min_by((.PY_VER | split(\".\") | map(tonumber)), (.CUDA_VER | split(\".\") | map(-tonumber)))]" >> $GITHUB_OUTPUT @@ -481,7 +481,7 @@ jobs: script: ci/build_wheel_cuopt.sh package-name: cuopt package-type: python - # abi3 wheel: build one per CUDA x ARCH, using the minimum supported Python + # abi3 wheel: build one per ARCH + CUDA major, using the minimum supported Python matrix_filter: ${{ needs.compute-matrix-filters.outputs.cuopt_filter }} wheel-tests-cuopt: needs: [wheel-build-cuopt, wheel-build-cuopt-sh-client, changed-files] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7454489f75..f23b0cf181 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -216,8 +216,8 @@ To build all libraries and tests, simply run - **Note**: if Cython files (`*.pyx` or `*.pxd`) have changed, the Python build must be rerun. - **Note**: the `cuopt` wheel is built against the CPython Limited API (abi3), so Cython code must - avoid CPython APIs outside it. Using one surfaces as a compile error under `-DPy_LIMITED_API`, - not at runtime. + use only APIs the Limited API exposes. An unsupported API fails to compile under + `-DPy_LIMITED_API` rather than failing at runtime. To run the C++ tests, run diff --git a/ci/validate_wheel.sh b/ci/validate_wheel.sh index a603c69098..685d4b920d 100755 --- a/ci/validate_wheel.sh +++ b/ci/validate_wheel.sh @@ -46,3 +46,13 @@ rapids-logger "validate packages with 'twine'" twine check \ --strict \ "$(echo "${wheel_dir_relative_path}"/*.whl)" + +rapids-logger "validate packages with 'abi3audit'" + +# 'abi3audit' fails on wheels with DSOs that lack an ABI tag (e.g. 'lib*' wheels). +# Filtering by '*abi*' avoids those. +find \ + "${wheel_dir_relative_path}" \ + -type f \ + -name '*abi*' \ + -exec abi3audit --strict --summary --verbose '{}' \+ diff --git a/skills/cuopt-developer/references/contributing.md b/skills/cuopt-developer/references/contributing.md index c717a88fb1..583cc2dc38 100644 --- a/skills/cuopt-developer/references/contributing.md +++ b/skills/cuopt-developer/references/contributing.md @@ -109,7 +109,7 @@ Follow YAGNI strictly here — flags, fallbacks, env-var overrides, and config k A few non-YAGNI points worth keeping in mind: - Prefer extending an existing script over adding a new one. -- For build/CI conventions shared across RAPIDS (wheel packaging, artifact naming, matrix filters), read the reference implementation in `rapidsai/rmm` or `rapidsai/cudf` on `main` before writing anything. cuOpt's `ci/` scripts and shared-workflow inputs are near-copies of theirs, and gha-tools expects exact conventions — an equivalent-but-different local invention silently breaks the download side, which looks for the name the build side wrote. +- For build/CI conventions shared across RAPIDS (wheel packaging, artifact naming, matrix filters), read the reference implementation in `rapidsai/rmm` or `NVIDIA/cudf` on `main` before writing anything. cuOpt's `ci/` scripts and shared-workflow inputs are near-copies of theirs, and gha-tools expects exact conventions — an equivalent-but-different local invention silently breaks the download side, which looks for the name the build side wrote. - Validate inputs at the top, before any expensive work. - One shell command per line over chained `&&`; no comments that restate the next line. - Keep informational CI jobs (reporting, dashboards, comment posting) out of any required-checks list. From 63de0b26c29b7b516e7e3cb38d2734620473d954 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 18 Aug 2026 10:08:54 -0500 Subject: [PATCH 5/8] Assert cuopt produces an abi3 wheel during validation Tighten the abi3audit selection from '*abi*' to '*-abi3-*.whl', and fail when 'python/cuopt' yields no abi3 wheel. Dropping 'wheel.py-api' would otherwise skip the audit silently and leave CI green. The check is scoped to 'python/cuopt' because libcuopt, cuopt_server and cuopt_sh_client share this script and are all 'py3-none'. Signed-off-by: Ramakrishna Prabhu --- ci/validate_wheel.sh | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/ci/validate_wheel.sh b/ci/validate_wheel.sh index 685d4b920d..2e8e84564d 100755 --- a/ci/validate_wheel.sh +++ b/ci/validate_wheel.sh @@ -49,10 +49,21 @@ twine check \ rapids-logger "validate packages with 'abi3audit'" -# 'abi3audit' fails on wheels with DSOs that lack an ABI tag (e.g. 'lib*' wheels). -# Filtering by '*abi*' avoids those. -find \ - "${wheel_dir_relative_path}" \ - -type f \ - -name '*abi*' \ - -exec abi3audit --strict --summary --verbose '{}' \+ +# 'abi3audit' fails on wheels with DSOs that lack an ABI tag, so only the abi3 wheels +# are audited. Of the packages sharing this script, only 'cuopt' builds one; the rest +# are 'py3-none'. +abi3_wheels=() +while IFS= read -r -d '' wheel; do + abi3_wheels+=("${wheel}") +done < <(find "${wheel_dir_relative_path}" -type f -name '*-abi3-*.whl' -print0) + +# Guard against 'cuopt' silently losing its abi3 tag: without this, dropping +# 'wheel.py-api' would skip the audit entirely and leave CI green. +if [[ "${package_dir}" == "python/cuopt" ]] && [[ "${#abi3_wheels[@]}" -eq 0 ]]; then + rapids-echo-stderr "expected an abi3 wheel in '${wheel_dir_relative_path}', found none" + exit 1 +fi + +if [[ "${#abi3_wheels[@]}" -gt 0 ]]; then + abi3audit --strict --summary --verbose "${abi3_wheels[@]}" +fi From b9165cc064ee7a8c334c6c25b481923e68c91ff4 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 18 Aug 2026 11:00:23 -0500 Subject: [PATCH 6/8] Revert the cuopt wheel matrix filter to the standard RAPIDS form The wheel build matrix intentionally carries a single CUDA minor per major, so grouping on the CUDA major added no coverage. Match the form used in cudf and the other RAPIDS repos instead. Signed-off-by: Ramakrishna Prabhu --- .github/workflows/build.yaml | 6 ++---- .github/workflows/pr.yaml | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a1f09d5288..aae5fee0e7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -157,10 +157,8 @@ jobs: script: ci/build_wheel_cuopt.sh package-name: cuopt package-type: python - # abi3 wheel: build one per ARCH + CUDA major, using the minimum supported Python. - # Grouping on the CUDA major (not the full version) keeps one job per published - # artifact name, which only encodes the major. - matrix_filter: group_by([.ARCH, (.CUDA_VER|split(".")|map(tonumber)|.[0])]) | map(min_by((.PY_VER|split(".")|map(tonumber)), (.CUDA_VER|split(".")|map(-tonumber)))) + # Build a wheel for each CUDA x ARCH x minimum supported Python version + matrix_filter: group_by({CUDA_VER, ARCH}) | map(min_by(.PY_VER | split(".") | map(tonumber))) wheel-publish-cuopt: needs: wheel-build-cuopt permissions: diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 342682ae06..3d2ba4b3f5 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -57,7 +57,7 @@ jobs: id: set-filters run: | echo "libcuopt_filter=group_by([.ARCH, (.CUDA_VER|split(\".\")|map(tonumber)|.[0])]) | map(max_by(.PY_VER|split(\".\")|map(tonumber)))" >> $GITHUB_OUTPUT - echo "cuopt_filter=group_by([.ARCH, (.CUDA_VER|split(\".\")|map(tonumber)|.[0])]) | map(min_by((.PY_VER|split(\".\")|map(tonumber)), (.CUDA_VER|split(\".\")|map(-tonumber))))" >> $GITHUB_OUTPUT + echo "cuopt_filter=group_by({CUDA_VER, ARCH}) | map(min_by(.PY_VER | split(\".\") | map(tonumber)))" >> $GITHUB_OUTPUT echo "cuopt_server_filter=map(select(.ARCH == \"amd64\")) | group_by(.CUDA_VER|split(\".\")|map(tonumber)|.[0]) | map(max_by([(.PY_VER|split(\".\")|map(tonumber)), (.CUDA_VER|split(\".\")|map(tonumber))]))" >> $GITHUB_OUTPUT echo "cuopt_server_test_filter=map(select(.ARCH == \"amd64\")) | group_by(.CUDA_VER | split(\".\") | map(tonumber) | .[0]) | map(max_by([(.PY_VER | split(\".\") | map(tonumber)), (.CUDA_VER | split(\".\") | map(tonumber))]))" >> $GITHUB_OUTPUT echo "cuopt_sh_client_filter=[map(select(.ARCH == \"amd64\")) | min_by((.PY_VER | split(\".\") | map(tonumber)), (.CUDA_VER | split(\".\") | map(-tonumber)))]" >> $GITHUB_OUTPUT @@ -481,7 +481,7 @@ jobs: script: ci/build_wheel_cuopt.sh package-name: cuopt package-type: python - # abi3 wheel: build one per ARCH + CUDA major, using the minimum supported Python + # Build a wheel for each CUDA x ARCH x minimum supported Python version matrix_filter: ${{ needs.compute-matrix-filters.outputs.cuopt_filter }} wheel-tests-cuopt: needs: [wheel-build-cuopt, wheel-build-cuopt-sh-client, changed-files] From 187f06a4668c8093f7aff451d9e1e7519baa4538 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 18 Aug 2026 11:56:26 -0500 Subject: [PATCH 7/8] Revert the abi3audit step to the standard RAPIDS form Drop the package-specific conditional so the set of packages sharing this script can change without anyone remembering to update it. The case it guarded is already caught: a cuopt wheel built without 'wheel.py-api' is tagged cp311-cp311, which fails to install in the Python 3.12+ wheel-test jobs. Signed-off-by: Ramakrishna Prabhu --- ci/validate_wheel.sh | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/ci/validate_wheel.sh b/ci/validate_wheel.sh index 2e8e84564d..685d4b920d 100755 --- a/ci/validate_wheel.sh +++ b/ci/validate_wheel.sh @@ -49,21 +49,10 @@ twine check \ rapids-logger "validate packages with 'abi3audit'" -# 'abi3audit' fails on wheels with DSOs that lack an ABI tag, so only the abi3 wheels -# are audited. Of the packages sharing this script, only 'cuopt' builds one; the rest -# are 'py3-none'. -abi3_wheels=() -while IFS= read -r -d '' wheel; do - abi3_wheels+=("${wheel}") -done < <(find "${wheel_dir_relative_path}" -type f -name '*-abi3-*.whl' -print0) - -# Guard against 'cuopt' silently losing its abi3 tag: without this, dropping -# 'wheel.py-api' would skip the audit entirely and leave CI green. -if [[ "${package_dir}" == "python/cuopt" ]] && [[ "${#abi3_wheels[@]}" -eq 0 ]]; then - rapids-echo-stderr "expected an abi3 wheel in '${wheel_dir_relative_path}', found none" - exit 1 -fi - -if [[ "${#abi3_wheels[@]}" -gt 0 ]]; then - abi3audit --strict --summary --verbose "${abi3_wheels[@]}" -fi +# 'abi3audit' fails on wheels with DSOs that lack an ABI tag (e.g. 'lib*' wheels). +# Filtering by '*abi*' avoids those. +find \ + "${wheel_dir_relative_path}" \ + -type f \ + -name '*abi*' \ + -exec abi3audit --strict --summary --verbose '{}' \+ From ba01e22eab86cecf02f8259ea8bb3ada5882412f Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Tue, 18 Aug 2026 14:34:55 -0500 Subject: [PATCH 8/8] Build conda python packages independently of the Python version Build one set of conda python packages per CUDA x arch instead of one per Python version, taking conda-python-build from 16 jobs to 4. All three python packages have to become version-independent for the matrix filter to be safe, since they share a single output channel: - cuopt: abi3 via 'version_independent: true', built against the minimum supported Python. 'python-gil' is dropped because 'python-abi3' already depends on it transitively, and cython is raised to >=3.2.2 for its Limited API support. - cuopt-server, cuopt-sh-client: 'noarch: python'. Neither builds extension modules. build.sh passes the abi3 floor only for cuopt, so the two pure packages keep using the plain install args. Depends on #1731, which sets 'wheel.py-api' in python/cuopt. Signed-off-by: Ramakrishna Prabhu --- .github/workflows/build.yaml | 2 ++ .github/workflows/pr.yaml | 2 ++ build.sh | 9 ++++++++- ci/build_docs.sh | 2 +- ci/build_python.sh | 2 +- ci/test_notebooks.sh | 2 +- ci/test_python.sh | 2 +- conda/recipes/cuopt-server/recipe.yaml | 7 +++---- conda/recipes/cuopt-sh-client/recipe.yaml | 7 +++---- conda/recipes/cuopt/recipe.yaml | 16 +++++++++++----- 10 files changed, 33 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index aae5fee0e7..45b7b461a3 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -81,6 +81,8 @@ jobs: date: ${{ inputs.date }} sha: ${{ inputs.sha }} script: ci/build_python.sh + # Build a conda package for each CUDA x ARCH x minimum supported Python version + matrix_filter: group_by({CUDA_VER, ARCH}) | map(min_by(.PY_VER | split(".") | map(tonumber))) upload-conda: needs: [cpp-build, python-build] permissions: diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 3d2ba4b3f5..7a64d7f083 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -412,6 +412,8 @@ jobs: build_type: pull-request build-datetime: ${{ needs.build-details.outputs.build-datetime }} script: ci/build_python.sh + # Build a conda package for each CUDA x ARCH x minimum supported Python version + matrix_filter: group_by({CUDA_VER, ARCH}) | map(min_by(.PY_VER | split(".") | map(tonumber))) conda-python-tests: needs: [conda-python-build, changed-files] permissions: diff --git a/build.sh b/build.sh index e6ed479192..21070c873e 100755 --- a/build.sh +++ b/build.sh @@ -432,8 +432,15 @@ fi if buildAll || hasArg cuopt; then cd "${REPODIR}"/python/cuopt + # Only 'cuopt' builds extension modules, so the stable ABI floor applies to it + # alone. If 'RAPIDS_PY_VERSION' is set, use it as that floor. + CUOPT_PYTHON_ARGS_FOR_INSTALL=("${PYTHON_ARGS_FOR_INSTALL[@]}") + if [ -n "${RAPIDS_PY_VERSION:-}" ]; then + CUOPT_PYTHON_ARGS_FOR_INSTALL+=(--config-settings "skbuild.wheel.py-api=cp${RAPIDS_PY_VERSION//./}") + fi + SKBUILD_CMAKE_ARGS="-DCMAKE_PREFIX_PATH=${INSTALL_PREFIX};-DCMAKE_LIBRARY_PATH=${LIBCUOPT_BUILD_DIR};-DCMAKE_CUDA_ARCHITECTURES=${CUOPT_CMAKE_CUDA_ARCHITECTURES};$(IFS=';'; echo "${EXTRA_CMAKE_ARGS[*]}")" \ - python "${PYTHON_ARGS_FOR_INSTALL[@]}" . + python "${CUOPT_PYTHON_ARGS_FOR_INSTALL[@]}" . fi # Build and install the cuopt_server Python package diff --git a/ci/build_docs.sh b/ci/build_docs.sh index c9fc8a5fe8..63abe3f57d 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -18,7 +18,7 @@ export RAPIDS_VERSION_MAJOR_MINOR rapids-logger "Downloading artifacts from previous jobs" CPP_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_cpp libcuopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") -PYTHON_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_python cuopt cuopt --py "$RAPIDS_PY_VERSION" --cuda "$RAPIDS_CUDA_VERSION")") +PYTHON_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")") rapids-logger "Generating conda environment YAML" diff --git a/ci/build_python.sh b/ci/build_python.sh index 461063a02c..ad72231f6d 100755 --- a/ci/build_python.sh +++ b/ci/build_python.sh @@ -67,5 +67,5 @@ rattler-build build --recipe conda/recipes/cuopt-sh-client \ # tracked in https://github.com/prefix-dev/rattler-build/issues/1424 rm -rf "$RAPIDS_CONDA_BLD_OUTPUT_DIR"/build_cache -RAPIDS_PACKAGE_NAME="$(rapids-artifact-name conda_python cuopt cuopt --py "$RAPIDS_PY_VERSION" --cuda "$RAPIDS_CUDA_VERSION")" +RAPIDS_PACKAGE_NAME="$(rapids-artifact-name conda_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")" export RAPIDS_PACKAGE_NAME diff --git a/ci/test_notebooks.sh b/ci/test_notebooks.sh index 461b8092d3..8285b3091f 100755 --- a/ci/test_notebooks.sh +++ b/ci/test_notebooks.sh @@ -12,7 +12,7 @@ conda config --set channel_priority strict rapids-logger "Downloading artifacts from previous jobs" CPP_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_cpp libcuopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") -PYTHON_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_python cuopt cuopt --py "$RAPIDS_PY_VERSION" --cuda "$RAPIDS_CUDA_VERSION")") +PYTHON_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")") rapids-logger "Generate notebook testing dependencies" diff --git a/ci/test_python.sh b/ci/test_python.sh index 06ce5a5616..ce3653a4ba 100755 --- a/ci/test_python.sh +++ b/ci/test_python.sh @@ -12,7 +12,7 @@ conda config --set channel_priority strict rapids-logger "Downloading artifacts from previous jobs" CPP_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_cpp libcuopt cuopt --cuda "$RAPIDS_CUDA_VERSION")") -PYTHON_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_python cuopt cuopt --py "$RAPIDS_PY_VERSION" --cuda "$RAPIDS_CUDA_VERSION")") +PYTHON_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_python cuopt cuopt --stable --cuda "$RAPIDS_CUDA_VERSION")") rapids-logger "Generate Python testing dependencies" rapids-dependency-file-generator \ diff --git a/conda/recipes/cuopt-server/recipe.yaml b/conda/recipes/cuopt-server/recipe.yaml index 7c77ee6e6e..f769e75bfc 100644 --- a/conda/recipes/cuopt-server/recipe.yaml +++ b/conda/recipes/cuopt-server/recipe.yaml @@ -5,8 +5,6 @@ schema_version: 1 context: version: ${{ env.get("RAPIDS_PACKAGE_VERSION") }} datetime_string: '${{ env.get("RAPIDS_DATETIME_STRING") }}' - py_version: ${{ env.get("RAPIDS_PY_VERSION") }} - py_buildstring: ${{ py_version | version_to_buildstring }} head_rev: '${{ git.head_rev(".")[:8] }}' package: @@ -17,7 +15,8 @@ source: path: ../../.. build: - string: py${{ py_buildstring }}_${{ datetime_string }}_${{ head_rev }} + noarch: python + string: ${{ datetime_string }}_${{ head_rev }} script: content: | # We pass an explicit matrix-entry here along with `disable-cuda=true` to @@ -28,7 +27,7 @@ build: requirements: host: - pip - - python-gil =${{ py_version }} + - python >=3.11 - rapids-build-backend >=0.4.0,<0.5.0 - setuptools>=77.0.0 run: diff --git a/conda/recipes/cuopt-sh-client/recipe.yaml b/conda/recipes/cuopt-sh-client/recipe.yaml index 5e603616fb..2493c058b6 100644 --- a/conda/recipes/cuopt-sh-client/recipe.yaml +++ b/conda/recipes/cuopt-sh-client/recipe.yaml @@ -6,8 +6,6 @@ context: version: ${{ env.get("RAPIDS_PACKAGE_VERSION") }} minor_version: ${{ (version | split("."))[:2] | join(".") }} datetime_string: '${{ env.get("RAPIDS_DATETIME_STRING") }}' - py_version: ${{ env.get("RAPIDS_PY_VERSION") }} - py_buildstring: ${{ py_version | version_to_buildstring }} head_rev: '${{ git.head_rev(".")[:8] }}' package: @@ -18,7 +16,8 @@ source: path: ../../.. build: - string: py${{ py_buildstring }}_${{ datetime_string }}_${{ head_rev }} + noarch: python + string: ${{ datetime_string }}_${{ head_rev }} script: content: | ./build.sh cuopt_sh_client @@ -26,7 +25,7 @@ build: requirements: host: - pip - - python-gil =${{ py_version }} + - python >=3.11 - setuptools>=77.0.0 run: - msgpack-python =1.2.1 diff --git a/conda/recipes/cuopt/recipe.yaml b/conda/recipes/cuopt/recipe.yaml index 776e8e2d7a..8dbd7d5a23 100644 --- a/conda/recipes/cuopt/recipe.yaml +++ b/conda/recipes/cuopt/recipe.yaml @@ -8,8 +8,9 @@ context: cuda_version: ${{ (env.get("RAPIDS_CUDA_VERSION") | split("."))[:2] | join(".") }} cuda_major: '${{ (env.get("RAPIDS_CUDA_VERSION") | split("."))[0] }}' datetime_string: '${{ env.get("RAPIDS_DATETIME_STRING") }}' - py_version: ${{ env.get("RAPIDS_PY_VERSION") }} - py_buildstring: ${{ py_version | version_to_buildstring }} + py_abi_min: ${{ env.get("RAPIDS_PY_VERSION") }} + py_buildstring: ${{ py_abi_min | version_to_buildstring }} + py_runtime_latest: "3.14" head_rev: '${{ git.head_rev(".")[:8] }}' package: @@ -20,7 +21,9 @@ source: path: ../../.. build: - string: cuda${{ cuda_major }}_py${{ py_buildstring }}_${{ datetime_string }}_${{ head_rev }} + python: + version_independent: true + string: cuda${{ cuda_major }}_cp${{ py_buildstring }}_abi3_${{ datetime_string }}_${{ head_rev }} dynamic_linking: overlinking_behavior: error prefix_detection: @@ -42,6 +45,7 @@ build: CMAKE_GENERATOR: ${{ env.get("CMAKE_GENERATOR") }} NVCC_APPEND_FLAGS: ${{ env.get("NVCC_APPEND_FLAGS", default="") }} PARALLEL_LEVEL: ${{ env.get("PARALLEL_LEVEL", default="8") }} + RAPIDS_PY_VERSION: ${{ py_abi_min }} SCCACHE_BUCKET: ${{ env.get("SCCACHE_BUCKET", default="") }} SCCACHE_DIST_AUTH_TYPE: ${{ env.get("SCCACHE_DIST_AUTH_TYPE", default="token") }} SCCACHE_DIST_FALLBACK_TO_LOCAL_COMPILE: ${{ env.get("SCCACHE_DIST_FALLBACK_TO_LOCAL_COMPILE", default="false") }} @@ -71,11 +75,12 @@ requirements: - ${{ stdlib("c") }} host: - cuda-version =${{ cuda_version }} - - cython >=3.0.0 + - cython >=3.2.2 - libcuopt =${{ version }} - pip - pylibraft =${{ minor_version }} - - python-gil =${{ py_version }} + - python =${{ py_abi_min }} + - python-abi3 ${{ py_abi_min }}.* - rapids-build-backend >=0.4.0,<0.5.0 - rmm =${{ minor_version }} - scikit-build-core>=0.11.0 @@ -105,6 +110,7 @@ tests: - python: imports: - cuopt + python_version: ${{ py_runtime_latest }}.* pip_check: false about: