diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 1746cecd0e..1a128660bc 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -20,6 +20,7 @@ jobs: - conda-cpp-tests - conda-cpp-checks - conda-python-build + - conda-python-import-tests - conda-python-tests - rocky8-clib-standalone-build - rocky8-clib-tests @@ -170,6 +171,7 @@ jobs: - '!ci/run_cuvs_pytests.sh' - '!ci/test_cpp.sh' - '!ci/test_python.sh' + - '!ci/test_python_imports.sh' - '!ci/test_wheel_cuvs.sh' - '!ci/validate_wheel.sh' - '!conda/environments/**' @@ -285,6 +287,7 @@ jobs: - '!ci/test_java.sh' - '!ci/test_lucene.sh' - '!ci/test_python.sh' + - '!ci/test_python_imports.sh' - '!ci/test_standalone_c.sh' - '!ci/validate_wheel.sh' - '!CODEOWNERS' @@ -345,6 +348,7 @@ jobs: - '!ci/test_java.sh' - '!ci/test_lucene.sh' - '!ci/test_python.sh' + - '!ci/test_python_imports.sh' - '!ci/test_wheel_cuvs.sh' - '!ci/validate_wheel.sh' - '!docs/**' @@ -398,6 +402,7 @@ jobs: - '!ci/test_java.sh' - '!ci/test_lucene.sh' - '!ci/test_python.sh' + - '!ci/test_python_imports.sh' - '!ci/test_standalone_c.sh' - '!ci/test_wheel_cuvs.sh' - '!ci/validate_wheel.sh' @@ -549,6 +554,26 @@ jobs: # 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))) matrix_type: pull-request + conda-python-import-tests: + # Installs the built Python packages with only their runtime dependencies and imports them on a + # CPU-only runner. This used to run as recipe tests inside conda-python-build; running it here + # keeps it off the critical path of the GPU test jobs. + needs: [conda-python-build, changed-files] + permissions: + actions: read + contents: read + id-token: write + packages: read + pull-requests: read + secrets: inherit # zizmor: ignore[secrets-inherit] + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@main + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python_conda + with: + build_type: pull-request + node_type: cpu8 + arch: "amd64" + container_image: "rapidsai/ci-conda:26.12-latest" + script: "ci/test_python_imports.sh" conda-python-tests: needs: [conda-python-build, changed-files] permissions: diff --git a/ci/test_python_imports.sh b/ci/test_python_imports.sh new file mode 100755 index 0000000000..f6965be1a1 --- /dev/null +++ b/ci/test_python_imports.sh @@ -0,0 +1,90 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Install the built cuVS Conda packages with only their runtime dependencies +# and import them on a CPU-only runner. This replaces the import tests that +# used to run inside each recipe build, taking them off the critical path of +# the GPU test jobs, and checks that nothing links against the CUDA driver or +# initializes CUDA at import time. + +set -euo pipefail + +source /opt/conda/etc/profile.d/conda.sh + +rapids-logger "Downloading built Conda packages" +CPP_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_cpp libcuvs cuvs --cuda "$RAPIDS_CUDA_VERSION")") +PYTHON_CHANNEL=$(rapids-download-from-github "$(rapids-artifact-name conda_python cuvs cuvs --stable --cuda "$RAPIDS_CUDA_VERSION")") + +# Create an environment holding the given built packages and their runtime +# dependencies, then import the given Python modules in it. +# +# usage: import_packages [ ...] -- [ ...] +import_packages() { + local env_name="$1" + shift + local packages=() + while [[ "$1" != "--" ]]; do + packages+=("$1") + shift + done + shift + + rapids-logger "Install ${packages[*]} with only their runtime dependencies" + rapids-mamba-retry create --yes --name "${env_name}" --strict-channel-priority \ + --override-channels \ + --channel "${PYTHON_CHANNEL}" --channel "${CPP_CHANNEL}" \ + --channel rapidsai-nightly --channel conda-forge \ + "python=${RAPIDS_PY_VERSION}" "${packages[@]}" + + set +u + conda activate "${env_name}" + set -u + rapids-print-env + + for module in "$@"; do + rapids-logger "Import ${module} on a CPU-only runner" + python -c "import ${module}" + done + + rapids-logger "Check that cuvs_bench ships its dataset configuration" + python -c "from importlib.resources import files; path = files('cuvs_bench') / 'config' / 'datasets' / 'datasets.yaml'; assert path.is_file(), path" + + set +u + conda deactivate + set -u +} + +# cuvs-bench and cuvs-bench-cpu both provide the cuvs_bench module, so they +# are checked in separate environments. +import_packages python_imports \ + "${PYTHON_CHANNEL}"/linux-64/cuvs-[0-9]*.conda \ + "${PYTHON_CHANNEL}"/linux-64/cuvs-bench-[0-9]*.conda \ + -- \ + cuvs \ + cuvs.cluster \ + cuvs.common \ + cuvs.distance \ + cuvs.neighbors \ + cuvs.preprocessing \ + cuvs_bench \ + cuvs_bench.generate_groundtruth \ + cuvs_bench.get_dataset \ + cuvs_bench.plot \ + cuvs_bench.run \ + cuvs_bench.split_groundtruth + +# cuvs-bench-cpu is only built for one CUDA major version (see ci/build_python.sh). +if compgen -G "${PYTHON_CHANNEL}/linux-64/cuvs-bench-cpu-[0-9]*.conda" > /dev/null; then + import_packages python_imports_cpu \ + "${PYTHON_CHANNEL}"/linux-64/cuvs-bench-cpu-[0-9]*.conda \ + -- \ + cuvs_bench \ + cuvs_bench.generate_groundtruth \ + cuvs_bench.get_dataset \ + cuvs_bench.plot \ + cuvs_bench.run \ + cuvs_bench.split_groundtruth +else + rapids-logger "No cuvs-bench-cpu package was built for CUDA ${RAPIDS_CUDA_VERSION}; skipping" +fi diff --git a/conda/recipes/cuvs-bench-cpu/recipe.yaml b/conda/recipes/cuvs-bench-cpu/recipe.yaml index 62c4254ae5..2672afa57e 100644 --- a/conda/recipes/cuvs-bench-cpu/recipe.yaml +++ b/conda/recipes/cuvs-bench-cpu/recipe.yaml @@ -9,7 +9,6 @@ context: linux64: ${{ linux and x86_64 }} py_abi_min: ${{ env.get("RAPIDS_PY_VERSION") }} py_buildstring: ${{ py_abi_min | version_to_buildstring }} - py_runtime_latest: "3.14" package: name: cuvs-bench-cpu @@ -118,19 +117,6 @@ requirements: - libcublas - mkl # relaxes pin from host `mkl-devel` - -tests: - - python: - imports: - - cuvs_bench - - cuvs_bench.generate_groundtruth - - cuvs_bench.get_dataset - - cuvs_bench.plot - - cuvs_bench.run - - cuvs_bench.split_groundtruth - python_version: ${{ py_runtime_latest }}.* - pip_check: false - about: homepage: ${{ load_from_file("python/cuvs_bench/pyproject.toml").project.urls.Homepage }} license: ${{ load_from_file("python/cuvs_bench/pyproject.toml").project.license }} diff --git a/conda/recipes/cuvs-bench/recipe.yaml b/conda/recipes/cuvs-bench/recipe.yaml index 655164f962..ea94be7d17 100644 --- a/conda/recipes/cuvs-bench/recipe.yaml +++ b/conda/recipes/cuvs-bench/recipe.yaml @@ -11,7 +11,6 @@ context: head_rev: '${{ git.head_rev(".")[:8] }}' py_abi_min: ${{ env.get("RAPIDS_PY_VERSION") }} py_buildstring: ${{ py_abi_min | version_to_buildstring }} - py_runtime_latest: "3.14" package: name: cuvs-bench @@ -60,21 +59,6 @@ requirements: by_name: - cuda-version -tests: - - package_contents: - files: - - site-packages/cuvs_bench/config/datasets/datasets.yaml - - python: - imports: - - cuvs_bench - - cuvs_bench.generate_groundtruth - - cuvs_bench.get_dataset - - cuvs_bench.plot - - cuvs_bench.run - - cuvs_bench.split_groundtruth - python_version: ${{ py_runtime_latest }}.* - pip_check: false - about: homepage: ${{ load_from_file("python/cuvs_bench/pyproject.toml").project.urls.Homepage }} license: ${{ load_from_file("python/cuvs_bench/pyproject.toml").project.license }} diff --git a/conda/recipes/cuvs/recipe.yaml b/conda/recipes/cuvs/recipe.yaml index b786c176c0..9b21460f6a 100644 --- a/conda/recipes/cuvs/recipe.yaml +++ b/conda/recipes/cuvs/recipe.yaml @@ -11,7 +11,6 @@ context: head_rev: '${{ git.head_rev(".")[:8] }}' py_abi_min: ${{ env.get("RAPIDS_PY_VERSION") }} py_buildstring: ${{ py_abi_min | version_to_buildstring }} - py_runtime_latest: "3.14" package: name: cuvs @@ -99,13 +98,6 @@ requirements: - cuda-version - cuda-cudart -tests: - - python: - imports: - - cuvs - python_version: ${{ py_runtime_latest }}.* - pip_check: false - about: homepage: ${{ load_from_file("python/cuvs/pyproject.toml").project.urls.Homepage }} license: ${{ load_from_file("python/cuvs/pyproject.toml").project.license }}