From 227fe42f2f503173f497e7e43010de2743e2f0d9 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 30 Oct 2026 22:44:44 -0700 Subject: [PATCH 1/2] ci: plan builds and artifact reuse per bindings root Signed-off-by: Ralf W. Grosse-Kunstleve --- .github/workflows/build-wheel.yml | 422 +++++++++++------- .github/workflows/ci-pixi-source-test.yml | 327 +++++++++----- .github/workflows/ci.yml | 267 ++++++----- .github/workflows/test-sdist-linux.yml | 29 +- .github/workflows/test-sdist-windows.yml | 29 +- .github/workflows/test-wheel-linux.yml | 168 +++---- .github/workflows/test-wheel-windows.yml | 170 +++---- ci/tools/bindings_config.py | 61 +++ ci/tools/compute_ci_plan.py | 261 ++++++++--- ci/tools/env-vars | 190 +++----- ci/tools/run-tests | 57 ++- ci/tools/select_test_matrix.py | 113 +++++ ci/tools/tests/AGENTS.md | 20 + ci/tools/tests/test_bindings_config.py | 13 - ci/tools/tests/test_compute_ci_plan.py | 329 +++++++------- ci/tools/tests/test_merge_cuda_core_wheels.py | 38 ++ ci/tools/tests/test_select_test_matrix.py | 71 +++ 17 files changed, 1616 insertions(+), 949 deletions(-) create mode 100644 ci/tools/select_test_matrix.py create mode 100644 ci/tools/tests/AGENTS.md create mode 100644 ci/tools/tests/test_merge_cuda_core_wheels.py create mode 100644 ci/tools/tests/test_select_test_matrix.py diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index bc6d43a2392..2e62990a42e 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -8,17 +8,14 @@ on: host-platform: required: true type: string - cuda-version: - required: true - type: string - prev-cuda-version: + bindings-config: + description: Normalized CUDA bindings package-root registry JSON. required: true type: string workplan: - description: JSON workplan. An empty value builds and tests everything. - required: false + description: JSON workplan. + required: true type: string - default: "" single-cuda-major: description: "Build wheels for only the current CUDA major; skip the prior-major build and wheel merge" required: false @@ -40,24 +37,8 @@ permissions: jobs: build: env: - BUILD_PATHFINDER: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.pathfinder.needs_build }} - BUILD_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_build }} - BUILD_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_build }} - BUILD_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.needs_build }} - TEST_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_test }} - TEST_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_test }} - TEST_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.needs_test }} - BUILD_MAINTENANCE_BINDINGS: >- - ${{ !inputs.single-cuda-major && - (inputs.workplan == '' || - fromJSON(inputs.workplan).modules.bindings.needs_build || - fromJSON(inputs.workplan).modules.bindings.needs_test || - fromJSON(inputs.workplan).modules.core.needs_build || - fromJSON(inputs.workplan).modules.core.needs_test || - fromJSON(inputs.workplan).modules.python.needs_build || - fromJSON(inputs.workplan).modules.python.needs_test) }} - BASELINE_RUN_ID: ${{ inputs.workplan != '' && fromJSON(inputs.workplan).baseline.run_id || '' }} - BASELINE_SHA: ${{ inputs.workplan != '' && fromJSON(inputs.workplan).baseline.sha || '' }} + BASELINE_RUN_ID: ${{ fromJSON(inputs.workplan).baseline.run_id }} + BASELINE_SHA: ${{ fromJSON(inputs.workplan).baseline.sha }} strategy: fail-fast: false matrix: @@ -92,15 +73,48 @@ jobs: # the job level instead of linking into the diff. run: echo "::add-matcher::.github/matchers/compiler.json" + - name: Resolve package-specific workplan gates + env: + BINDINGS_CONFIG: ${{ inputs.bindings-config }} + WORKPLAN: ${{ inputs.workplan }} + run: | + set -euo pipefail + jq -er --argjson config "$BINDINGS_CONFIG" ' + . as $plan + | ($config.package_roots | map(select(.release_status == "current")) + | if length == 1 then .[0] else error("expected one current bindings package root") end) as $current + | ($config.package_roots | map(select(.release_status == "maintenance")) + | if length == 1 then .[0] else error("expected one maintenance bindings package root") end) as $maintenance + | { + PATHFINDER_SOURCE: $plan.sources.pathfinder, + BUILD_PATHFINDER: $plan.modules.pathfinder.needs_build, + BUILD_BINDINGS: $plan.modules.bindings.needs_build, + BUILD_BINDINGS_CURRENT: $plan.modules.bindings.package_roots[$current.package_root].needs_build, + BUILD_BINDINGS_MAINTENANCE: $plan.modules.bindings.package_roots[$maintenance.package_root].needs_build, + BUILD_CORE: $plan.modules.core.needs_build, + BUILD_CUDA_PYTHON: $plan.modules.python.needs_build, + BUILD_CUDA_PYTHON_CURRENT: $plan.modules.python.package_roots[$current.package_root].needs_build, + BUILD_CUDA_PYTHON_MAINTENANCE: $plan.modules.python.package_roots[$maintenance.package_root].needs_build, + TEST_BINDINGS: $plan.modules.bindings.needs_test, + TEST_BINDINGS_CURRENT: $plan.modules.bindings.package_roots[$current.package_root].needs_test, + TEST_BINDINGS_MAINTENANCE: $plan.modules.bindings.package_roots[$maintenance.package_root].needs_test, + TEST_CORE: $plan.modules.core.needs_test, + TEST_CORE_CURRENT: $plan.modules.core.cuda_majors[$current.cuda_variant].needs_test, + TEST_CORE_MAINTENANCE: $plan.modules.core.cuda_majors[$maintenance.cuda_variant].needs_test + } + | to_entries[] + | if .value == null then error("missing workplan value for \(.key)") else "\(.key)=\(.value)" end + ' <<< "$WORKPLAN" >> "$GITHUB_ENV" + - name: Install rapidsai/sccache - if: ${{ startsWith(inputs.host-platform, 'linux') && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true' || env.BUILD_MAINTENANCE_BINDINGS == 'true') }} + if: ${{ startsWith(inputs.host-platform, 'linux') && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') }} uses: ./.github/actions/install-sccache # The runner only exposes these values to actions, not to `run:` steps or # the cibuildwheel container, so re-export them for sccache's GHA backend. # xref: https://github.com/orgs/community/discussions/42856#discussioncomment-7678867 - name: Adding addtional GHA cache-related env vars - if: ${{ env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true' || env.BUILD_MAINTENANCE_BINDINGS == 'true' }} + if: ${{ env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true' }} uses: actions/github-script@v9 with: script: | @@ -159,14 +173,13 @@ jobs: - name: Set environment variables env: - CUDA_VER: ${{ inputs.cuda-version }} HOST_PLATFORM: ${{ inputs.host-platform }} PY_VER: ${{ matrix.python-version }} SHA: ${{ github.sha }} run: ./ci/tools/env-vars build - name: Prepare local maintenance SCM milestone - if: ${{ env.BUILD_MAINTENANCE_BINDINGS == 'true' }} + if: ${{ !inputs.single-cuda-major && (env.BUILD_BINDINGS_MAINTENANCE == 'true' || env.BUILD_CUDA_PYTHON_MAINTENANCE == 'true') }} run: | python -m ci.tools.bindings_scm_version \ --package-root "${MAINTENANCE_BINDINGS_ROOT}" --sha "${{ github.sha }}" @@ -188,7 +201,7 @@ jobs: popd - name: Download reusable cuda.pathfinder wheel - if: ${{ env.BUILD_PATHFINDER != 'true' }} + if: ${{ env.BUILD_PATHFINDER != 'true' && env.BASELINE_RUN_ID != '' }} uses: $/.github/actions/download-artifact-retry with: name: cuda-pathfinder-wheel @@ -197,6 +210,7 @@ jobs: run-id: ${{ env.BASELINE_RUN_ID }} - name: List the cuda.pathfinder artifacts directory + if: ${{ env.BUILD_PATHFINDER == 'true' || env.BASELINE_RUN_ID != '' }} run: | if [[ "${{ inputs.host-platform }}" == win* ]]; then export CHOWN=chown @@ -214,41 +228,51 @@ jobs: twine check --strict cuda_pathfinder/*.whl - name: Constrain builds to the local cuda.pathfinder wheel - if: ${{ env.BUILD_BINDINGS == 'true' || env.BUILD_MAINTENANCE_BINDINGS == 'true' }} + if: ${{ env.BUILD_BINDINGS == 'true' }} run: | pathfinder_wheels=(cuda_pathfinder/cuda_pathfinder-*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" mkdir -p wheel-constraints - if [[ "${{ inputs.host-platform }}" == win* ]]; then - pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" + if [[ "$PATHFINDER_SOURCE" == "artifact" ]]; then + if [[ "${#pathfinder_wheels[@]}" -ne 1 || ! -f "${pathfinder_wheels[0]}" ]]; then + echo "Expected exactly one cuda.pathfinder wheel" >&2 + exit 1 + fi + if [[ "${{ inputs.host-platform }}" == win* ]]; then + pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" + else + pathfinder_uri="file:///host$(realpath "${pathfinder_wheels[0]}")" + fi + printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" > wheel-constraints/cuda-bindings.txt + elif [[ "$PATHFINDER_SOURCE" == "published" ]]; then + : > wheel-constraints/cuda-bindings.txt else - pathfinder_uri="file:///host$(realpath "${pathfinder_wheels[0]}")" + echo "Unknown cuda.pathfinder source: $PATHFINDER_SOURCE" >&2 + exit 1 fi - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" | tee wheel-constraints/cuda-bindings.txt + cat wheel-constraints/cuda-bindings.txt - name: Upload cuda.pathfinder build artifacts - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + if: ${{ (env.BUILD_PATHFINDER == 'true' || env.BASELINE_RUN_ID != '') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: cuda-pathfinder-wheel path: cuda_pathfinder/*.whl if-no-files-found: error - - name: Set up mini CTK - if: ${{ env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true' || env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' }} + - name: Set up current mini CTK + if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' || env.BUILD_CORE == 'true' || env.TEST_BINDINGS_CURRENT == 'true' || env.TEST_CORE_CURRENT == 'true' }} uses: ./.github/actions/fetch_ctk continue-on-error: false with: host-platform: ${{ inputs.host-platform }} - cuda-version: ${{ inputs.cuda-version }} + cuda-version: ${{ env.CURRENT_CUDA_VERSION }} - - name: Build cuda.bindings wheel - if: ${{ env.BUILD_BINDINGS == 'true' }} + - name: Build cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} wheel + if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' }} uses: pypa/cibuildwheel@b9e566a2b284570af229f6c2a33673410d0389a3 # v4.3.4 with: - package-dir: ./cuda_bindings_13/ - output-dir: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + package-dir: ./${{ env.CURRENT_BINDINGS_ROOT }}/ + output-dir: ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }} extras: uv python-path: ${{ steps.host-python.outputs.python-path }} env: &bindings-wheel-env @@ -280,43 +304,44 @@ jobs: "/host/${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_bindings.json && auditwheel repair -w {dest_dir} {wheel} - - name: Report sccache stats (cuda.bindings) - if: ${{ env.BUILD_BINDINGS == 'true' && !startsWith(inputs.host-platform, 'win') }} + - name: Report sccache stats (cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }}) + if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' && !startsWith(inputs.host-platform, 'win') }} uses: ./.github/actions/sccache-summary with: json-file: sccache_bindings.json - label: "cuda.bindings" - build-step: "Build cuda.bindings wheel" + label: "cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }}" + build-step: "Build cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} wheel" - - name: Download reusable cuda.bindings wheel - if: ${{ env.BUILD_BINDINGS != 'true' }} + - name: Download reusable cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} wheel + if: ${{ env.BUILD_BINDINGS_CURRENT != 'true' && env.BASELINE_RUN_ID != '' }} uses: $/.github/actions/download-artifact-retry with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_BASENAME }}-${{ env.BASELINE_SHA }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + name: ${{ env.CURRENT_BINDINGS_ARTIFACT_BASENAME }}-${{ env.BASELINE_SHA }} + path: ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }} github-token: ${{ github.token }} run-id: ${{ env.BASELINE_RUN_ID }} - - name: List the cuda.bindings artifacts directory + - name: List the cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} artifacts directory + if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' || env.BASELINE_RUN_ID != '' }} run: | if [[ "${{ inputs.host-platform }}" == win* ]]; then export CHOWN=chown else export CHOWN="sudo chown" fi - $CHOWN -R $(whoami) ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - ls -lahR ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} + $CHOWN -R $(whoami) ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }} + ls -lahR ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }} - - name: Check cuda.bindings wheel - if: ${{ env.BUILD_BINDINGS == 'true' }} + - name: Check cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} wheel + if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' }} run: | - twine check --strict ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl + twine check --strict ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}/*.whl - name: Constrain cuda.core to the local cuda.bindings wheel if: ${{ env.BUILD_CORE == 'true' }} run: | pathfinder_wheels=(cuda_pathfinder/cuda_pathfinder-*.whl) - bindings_wheels=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-"${BUILD_CUDA_MAJOR}".*.whl) + bindings_wheels=("${CURRENT_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-"${CURRENT_CUDA_MAJOR}".*.whl) test "${#pathfinder_wheels[@]}" -eq 1 test "${#bindings_wheels[@]}" -eq 1 test -f "${pathfinder_wheels[0]}" @@ -334,14 +359,15 @@ jobs: printf 'cuda-bindings @ %s\n' "${bindings_uri}" } | tee wheel-constraints/cuda-core.txt - - name: Upload cuda.bindings build artifacts + - name: Upload cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} build artifacts + if: ${{ env.BUILD_BINDINGS_CURRENT == 'true' || env.BASELINE_RUN_ID != '' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} - path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl + name: ${{ env.CURRENT_BINDINGS_ARTIFACT_NAME }} + path: ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}/*.whl if-no-files-found: error - - name: Build cuda.core wheel + - name: Build cuda.core ${{ env.CURRENT_CUDA_VARIANT }} wheel if: ${{ env.BUILD_CORE == 'true' }} uses: pypa/cibuildwheel@b9e566a2b284570af229f6c2a33673410d0389a3 # v4.3.4 with: @@ -357,7 +383,7 @@ jobs: CIBW_ENVIRONMENT_LINUX: > CUDA_PATH=/host/${{ env.CUDA_PATH }} CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} + CUDA_CORE_BUILD_MAJOR=${{ env.CURRENT_CUDA_MAJOR }} CUDA_PYTHON_WERROR=1 UV_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core.txt UV_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core.txt @@ -371,7 +397,7 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: > CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} + CUDA_CORE_BUILD_MAJOR=${{ env.CURRENT_CUDA_MAJOR }} CUDA_PYTHON_WERROR=1 UV_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core.txt)" UV_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core.txt)" @@ -387,7 +413,7 @@ jobs: with: json-file: sccache_core.json label: "cuda.core" - build-step: "Build cuda.core wheel" + build-step: "Build cuda.core ${{ env.CURRENT_CUDA_VARIANT }} wheel" - name: List the cuda.core artifacts directory and rename if: ${{ env.BUILD_CORE == 'true' }} @@ -400,12 +426,12 @@ jobs: $CHOWN -R $(whoami) ${{ env.CUDA_CORE_ARTIFACTS_DIR }} # Rename wheel to include CUDA version suffix - mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}" + mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${CURRENT_CUDA_MAJOR}" for wheel in ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl; do if [[ -f "${wheel}" ]]; then base_name=$(basename "${wheel}" .whl) - new_name="${base_name}.cu${BUILD_CUDA_MAJOR}.whl" - mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}/${new_name}" + new_name="${base_name}.cu${CURRENT_CUDA_MAJOR}.whl" + mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${CURRENT_CUDA_MAJOR}/${new_name}" echo "Renamed wheel to: ${new_name}" fi done @@ -413,7 +439,7 @@ jobs: ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - name: Download reusable cuda.core wheel - if: ${{ env.BUILD_CORE != 'true' }} + if: ${{ env.BUILD_CORE != 'true' && env.BASELINE_RUN_ID != '' }} uses: $/.github/actions/download-artifact-retry with: name: ${{ env.CUDA_CORE_ARTIFACT_BASENAME }}-${{ env.BASELINE_SHA }} @@ -421,68 +447,80 @@ jobs: github-token: ${{ github.token }} run-id: ${{ env.BASELINE_RUN_ID }} - # We only need/want a single pure python wheel, pick linux-64 index 0. - - name: Build and check maintenance cuda-python wheel - if: ${{ !inputs.single-cuda-major && (env.BUILD_PYTHON == 'true' || env.TEST_PYTHON == 'true') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + # We only need/want one copy of each pure Python wheel, so use linux-64 index 0. + - name: Build and check cuda-python ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel + if: ${{ !inputs.single-cuda-major && env.BUILD_CUDA_PYTHON_MAINTENANCE == 'true' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} run: | + mkdir -p "cuda_python/dist/${MAINTENANCE_CUDA_VARIANT}" pushd cuda_python CUDA_PYTHON_BUILD_MAJOR="${MAINTENANCE_CUDA_MAJOR}" \ - pip wheel -v --no-deps --wheel-dir "dist/cu${MAINTENANCE_CUDA_MAJOR}" . - twine check --strict "dist/cu${MAINTENANCE_CUDA_MAJOR}"/*.whl + pip wheel -v --no-deps --wheel-dir "dist/${MAINTENANCE_CUDA_VARIANT}" . + twine check --strict "dist/${MAINTENANCE_CUDA_VARIANT}"/*.whl popd - - name: Upload maintenance cuda-python wheel - if: ${{ !inputs.single-cuda-major && (env.BUILD_PYTHON == 'true' || env.TEST_PYTHON == 'true') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Download reusable cuda-python ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel + if: ${{ !inputs.single-cuda-major && env.BUILD_CUDA_PYTHON_MAINTENANCE != 'true' && env.BASELINE_RUN_ID != '' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + uses: $/.github/actions/download-artifact-retry + with: + name: ${{ env.MAINTENANCE_PYTHON_ARTIFACT_NAME }} + path: cuda_python/dist/${{ env.MAINTENANCE_CUDA_VARIANT }} + github-token: ${{ github.token }} + run-id: ${{ env.BASELINE_RUN_ID }} + + - name: Upload cuda-python ${{ env.MAINTENANCE_CUDA_MAJOR }} build artifacts + if: ${{ !inputs.single-cuda-major && (env.BUILD_CUDA_PYTHON_MAINTENANCE == 'true' || env.BASELINE_RUN_ID != '') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: cuda-python-wheel-cuda${{ env.MAINTENANCE_CUDA_VERSION }} - path: cuda_python/dist/cu${{ env.MAINTENANCE_CUDA_MAJOR }}/*.whl + name: ${{ env.MAINTENANCE_PYTHON_ARTIFACT_NAME }} + path: cuda_python/dist/${{ env.MAINTENANCE_CUDA_VARIANT }}/*.whl if-no-files-found: error - - name: Build and check cuda-python wheel - if: ${{ env.BUILD_PYTHON == 'true' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Build and check cuda-python ${{ env.CURRENT_CUDA_MAJOR }} wheel + if: ${{ env.BUILD_CUDA_PYTHON_CURRENT == 'true' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} run: | + mkdir -p "cuda_python/dist/${CURRENT_CUDA_VARIANT}" pushd cuda_python - pip wheel -v --no-deps . - twine check --strict *.whl + CUDA_PYTHON_BUILD_MAJOR="${CURRENT_CUDA_MAJOR}" \ + pip wheel -v --no-deps --wheel-dir "dist/${CURRENT_CUDA_VARIANT}" . + twine check --strict "dist/${CURRENT_CUDA_VARIANT}"/*.whl popd - - name: Download reusable cuda-python wheel - if: ${{ env.BUILD_PYTHON != 'true' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Download reusable cuda-python ${{ env.CURRENT_CUDA_MAJOR }} wheel + if: ${{ env.BUILD_CUDA_PYTHON_CURRENT != 'true' && env.BASELINE_RUN_ID != '' && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} uses: $/.github/actions/download-artifact-retry with: - name: cuda-python-wheel - path: cuda_python + name: ${{ env.CURRENT_PYTHON_ARTIFACT_NAME }} + path: cuda_python/dist/${{ env.CURRENT_CUDA_VARIANT }} github-token: ${{ github.token }} run-id: ${{ env.BASELINE_RUN_ID }} - - name: List the cuda-python artifacts directory - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} - run: | - if [[ "${{ inputs.host-platform }}" == win* ]]; then - export CHOWN=chown - else - export CHOWN="sudo chown" - fi - $CHOWN -R $(whoami) cuda_python/*.whl - ls -lahR cuda_python - - - name: Upload cuda-python build artifacts - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Upload cuda-python ${{ env.CURRENT_CUDA_MAJOR }} build artifacts + if: ${{ (env.BUILD_CUDA_PYTHON_CURRENT == 'true' || env.BASELINE_RUN_ID != '') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: cuda-python-wheel-cuda${{ inputs.cuda-version }} - path: cuda_python/*.whl + name: ${{ env.CURRENT_PYTHON_ARTIFACT_NAME }} + path: cuda_python/dist/${{ env.CURRENT_CUDA_VARIANT }}/*.whl if-no-files-found: error - - name: Upload legacy cuda-python artifact alias - if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + - name: Upload legacy CUDA13 metapackage alias + if: ${{ (env.BUILD_CUDA_PYTHON_CURRENT == 'true' || env.BASELINE_RUN_ID != '') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: cuda-python-wheel - path: cuda_python/*.whl + path: cuda_python/dist/${{ env.CURRENT_CUDA_VARIANT }}/*.whl if-no-files-found: error + - name: List the cuda-python artifacts directories + if: ${{ (env.BUILD_CUDA_PYTHON == 'true' || env.BASELINE_RUN_ID != '') && strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + run: | + if [[ "${{ inputs.host-platform }}" == win* ]]; then + export CHOWN=chown + else + export CHOWN="sudo chown" + fi + $CHOWN -R $(whoami) cuda_python/dist + ls -lahR cuda_python/dist + - name: Set up Python if: ${{ env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' }} uses: ./.github/actions/setup-hardened-python @@ -508,7 +546,19 @@ jobs: - name: Install cuda.pathfinder (required for next step) if: ${{ env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' }} run: | - pip install cuda_pathfinder/*.whl + pathfinder_wheels=(cuda_pathfinder/*.whl) + if [[ "$PATHFINDER_SOURCE" == "artifact" ]]; then + if [[ "${#pathfinder_wheels[@]}" -ne 1 || ! -f "${pathfinder_wheels[0]}" ]]; then + echo "Expected exactly one cuda.pathfinder wheel" >&2 + exit 1 + fi + pip install "${pathfinder_wheels[0]}" + elif [[ "$PATHFINDER_SOURCE" == "published" ]]; then + pip install cuda-pathfinder + else + echo "Unknown cuda.pathfinder source: $PATHFINDER_SOURCE" >&2 + exit 1 + fi - name: Hide GNU link.exe so Meson finds MSVC link.exe if: ${{ startsWith(inputs.host-platform, 'win') && (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true') }} @@ -517,28 +567,36 @@ jobs: mv "/c/Program Files/Git/usr/bin/link.exe" "/c/Program Files/Git/usr/bin/link.exe.bak" fi - - name: Build cuda.bindings Cython tests - if: ${{ env.TEST_BINDINGS == 'true' }} + - name: Build cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} Cython tests + if: ${{ env.TEST_BINDINGS_CURRENT == 'true' }} run: | - pip install ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl --group ./cuda_bindings_13/pyproject.toml:test - pushd ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }} + bindings_wheels=(${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}/*.whl) + test "${#bindings_wheels[@]}" -eq 1 + test -f "${bindings_wheels[0]}" + bindings_pyproject="${CURRENT_BINDINGS_ROOT}/pyproject.toml" + if grep -q '^\[dependency-groups\][[:space:]]*$' "${bindings_pyproject}"; then + python -m pip install "${bindings_wheels[0]}" --group "${bindings_pyproject}:test" + else + python -m pip install "${bindings_wheels[0]}[test]" + fi + pushd ${{ env.CURRENT_BINDINGS_CYTHON_TESTS_DIR }} bash build_tests.sh popd - - name: Upload cuda.bindings Cython tests - if: ${{ env.TEST_BINDINGS == 'true' }} + - name: Upload cuda.bindings ${{ env.CURRENT_CUDA_MAJOR }} Cython tests + if: ${{ env.TEST_BINDINGS_CURRENT == 'true' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}-tests - path: ${{ env.CUDA_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} + name: ${{ env.CURRENT_BINDINGS_ARTIFACT_NAME }}-tests + path: ${{ env.CURRENT_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} if-no-files-found: error - - name: Build cuda.core Cython tests - if: ${{ env.TEST_CORE == 'true' }} + - name: Build cuda.core ${{ env.CURRENT_CUDA_VARIANT }} Cython tests + if: ${{ env.TEST_CORE_CURRENT == 'true' }} run: | - pip install ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl + pip install ${{ env.CURRENT_BINDINGS_ARTIFACTS_DIR }}/*.whl if ${{ env.BUILD_CORE == 'true' }}; then - core_wheel=$(find "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_CUDA_MAJOR}" -maxdepth 1 -type f -name '*.whl' -print -quit) + core_wheel=$(find "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${CURRENT_CUDA_MAJOR}" -maxdepth 1 -type f -name '*.whl' -print -quit) else core_wheel=$(find "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" -maxdepth 1 -type f -name '*.whl' -print -quit) fi @@ -551,22 +609,30 @@ jobs: bash build_tests.sh popd - - name: Upload cuda.core Cython tests - if: ${{ env.TEST_CORE == 'true' }} + - name: Upload current cuda.core Cython tests + if: ${{ env.TEST_CORE_CURRENT == 'true' }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-${{ env.CURRENT_CUDA_VARIANT }}-tests + path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} + if-no-files-found: error + + - name: Upload legacy CUDA13 Core Cython alias + if: ${{ env.TEST_CORE_CURRENT == 'true' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-tests path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} if-no-files-found: error - # Note: This overwrites CUDA_PATH etc - - name: Set up mini CTK - if: ${{ !inputs.single-cuda-major && (env.BUILD_CORE == 'true' || env.TEST_CORE == 'true' || env.BUILD_MAINTENANCE_BINDINGS == 'true') }} + # Note: This overwrites CUDA_PATH etc. + - name: Set up maintenance mini CTK + if: ${{ !inputs.single-cuda-major && (env.BUILD_BINDINGS_MAINTENANCE == 'true' || env.BUILD_CORE == 'true' || env.TEST_BINDINGS_MAINTENANCE == 'true' || env.TEST_CORE_MAINTENANCE == 'true') }} uses: ./.github/actions/fetch_ctk continue-on-error: false with: host-platform: ${{ inputs.host-platform }} - cuda-version: ${{ inputs.prev-cuda-version }} + cuda-version: ${{ env.MAINTENANCE_CUDA_VERSION }} cuda-path: "./cuda_toolkit_prev" - name: Build cuda.core test binaries @@ -586,8 +652,8 @@ jobs: ${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/*.lib if-no-files-found: error - - name: Build maintenance cuda.bindings wheel - if: ${{ env.BUILD_MAINTENANCE_BINDINGS == 'true' }} + - name: Build cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel + if: ${{ !inputs.single-cuda-major && env.BUILD_BINDINGS_MAINTENANCE == 'true' }} uses: pypa/cibuildwheel@b9e566a2b284570af229f6c2a33673410d0389a3 # v4.3.4 with: package-dir: ./${{ env.MAINTENANCE_BINDINGS_ROOT }}/ @@ -596,38 +662,67 @@ jobs: python-path: ${{ steps.host-python.outputs.python-path }} env: *bindings-wheel-env - - name: Check maintenance cuda.bindings wheel - if: ${{ env.BUILD_MAINTENANCE_BINDINGS == 'true' }} - run: twine check --strict "${MAINTENANCE_BINDINGS_ARTIFACTS_DIR}"/*.whl + - name: Report sccache stats (cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }}) + if: ${{ !inputs.single-cuda-major && env.BUILD_BINDINGS_MAINTENANCE == 'true' && !startsWith(inputs.host-platform, 'win') }} + uses: ./.github/actions/sccache-summary + with: + json-file: sccache_bindings.json + label: "cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }}" + build-step: "Build cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel" + + - name: Download reusable cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel + if: ${{ !inputs.single-cuda-major && env.BUILD_BINDINGS_MAINTENANCE != 'true' && env.BASELINE_RUN_ID != '' }} + uses: $/.github/actions/download-artifact-retry + with: + name: ${{ env.MAINTENANCE_BINDINGS_ARTIFACT_BASENAME }}-${{ env.BASELINE_SHA }} + path: ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }} + github-token: ${{ github.token }} + run-id: ${{ env.BASELINE_RUN_ID }} + + - name: List the cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} artifacts directory + if: ${{ !inputs.single-cuda-major && (env.BUILD_BINDINGS_MAINTENANCE == 'true' || env.BASELINE_RUN_ID != '') }} + run: | + if [[ "${{ inputs.host-platform }}" == win* ]]; then + export CHOWN=chown + else + export CHOWN="sudo chown" + fi + $CHOWN -R $(whoami) ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }} + ls -lahR ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }} + + - name: Check cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} wheel + if: ${{ !inputs.single-cuda-major && env.BUILD_BINDINGS_MAINTENANCE == 'true' }} + run: twine check --strict ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}/*.whl - - name: Upload maintenance cuda.bindings wheel - if: ${{ env.BUILD_MAINTENANCE_BINDINGS == 'true' }} + - name: Upload cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} build artifacts + if: ${{ !inputs.single-cuda-major && (env.BUILD_BINDINGS_MAINTENANCE == 'true' || env.BASELINE_RUN_ID != '') }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ env.MAINTENANCE_BINDINGS_ARTIFACT_NAME }} path: ${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}/*.whl if-no-files-found: error - - name: Build maintenance cuda.bindings Cython tests - if: ${{ !inputs.single-cuda-major && env.TEST_BINDINGS == 'true' }} + - name: Build cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} Cython tests + if: ${{ !inputs.single-cuda-major && env.TEST_BINDINGS_MAINTENANCE == 'true' }} run: | pip uninstall -y cuda-bindings - bindings_wheels=("${MAINTENANCE_BINDINGS_ARTIFACTS_DIR}"/*.whl) + bindings_wheels=(${{ env.MAINTENANCE_BINDINGS_ARTIFACTS_DIR }}/*.whl) test "${#bindings_wheels[@]}" -eq 1 + test -f "${bindings_wheels[0]}" python -m pip install "${bindings_wheels[0]}" --group "${MAINTENANCE_BINDINGS_ROOT}/pyproject.toml:test" - pushd "${MAINTENANCE_BINDINGS_CYTHON_TESTS_DIR}" + pushd ${{ env.MAINTENANCE_BINDINGS_CYTHON_TESTS_DIR }} bash build_tests.sh popd - - name: Upload maintenance cuda.bindings Cython tests - if: ${{ !inputs.single-cuda-major && env.TEST_BINDINGS == 'true' }} + - name: Upload cuda.bindings ${{ env.MAINTENANCE_CUDA_MAJOR }} Cython tests + if: ${{ !inputs.single-cuda-major && env.TEST_BINDINGS_MAINTENANCE == 'true' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ env.MAINTENANCE_BINDINGS_ARTIFACT_NAME }}-tests path: ${{ env.MAINTENANCE_BINDINGS_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} if-no-files-found: error - - name: Constrain maintenance cuda.core to the same-run cuda.bindings wheel + - name: Constrain maintenance cuda.core to the local cuda.bindings wheel if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }} run: | pathfinder_wheels=(cuda_pathfinder/cuda_pathfinder-*.whl) @@ -647,9 +742,9 @@ jobs: { printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" printf 'cuda-bindings @ %s\n' "${bindings_uri}" - } | tee wheel-constraints/cuda-core-prev.txt + } | tee wheel-constraints/cuda-core-maintenance.txt - - name: Build cuda.core wheel + - name: Build cuda.core ${{ env.MAINTENANCE_CUDA_VARIANT }} wheel if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }} uses: pypa/cibuildwheel@b9e566a2b284570af229f6c2a33673410d0389a3 # v4.3.4 with: @@ -665,10 +760,10 @@ jobs: CIBW_ENVIRONMENT_LINUX: > CUDA_PATH=/host/${{ env.CUDA_PATH }} CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_PREV_CUDA_MAJOR }} + CUDA_CORE_BUILD_MAJOR=${{ env.MAINTENANCE_CUDA_MAJOR }} CUDA_PYTHON_WERROR=1 - UV_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-prev.txt - UV_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-prev.txt + UV_BUILD_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-maintenance.txt + UV_CONSTRAINT=/host/${{ github.workspace }}/wheel-constraints/cuda-core-maintenance.txt CC="/host/${{ env.SCCACHE_PATH }} cc" CXX="/host/${{ env.SCCACHE_PATH }} c++" SCCACHE_GHA_ENABLED=true @@ -679,23 +774,23 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: > CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} - CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_PREV_CUDA_MAJOR }} + CUDA_CORE_BUILD_MAJOR=${{ env.MAINTENANCE_CUDA_MAJOR }} CUDA_PYTHON_WERROR=1 - UV_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-prev.txt)" - UV_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-prev.txt)" + UV_BUILD_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-maintenance.txt)" + UV_CONSTRAINT="$(cygpath -w ./wheel-constraints/cuda-core-maintenance.txt)" # Dump sccache stats during repair; no dummy test stage. CIBW_REPAIR_WHEEL_COMMAND_LINUX: > "/host/${{ env.SCCACHE_PATH }}" --show-adv-stats && - "/host/${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_core_prev.json && + "/host/${{ env.SCCACHE_PATH }}" --show-stats --stats-format=json > /host/${{ github.workspace }}/sccache_core_maintenance.json && auditwheel repair -w {dest_dir} {wheel} - - name: Report sccache stats (cuda.core prev) + - name: Report sccache stats (maintenance cuda.core) if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' && !startsWith(inputs.host-platform, 'win') }} uses: ./.github/actions/sccache-summary with: - json-file: sccache_core_prev.json - label: "cuda.core (prev CTK)" - build-step: "Build cuda.core wheel" + json-file: sccache_core_maintenance.json + label: "cuda.core (maintenance CTK)" + build-step: "Build cuda.core ${{ env.MAINTENANCE_CUDA_VARIANT }} wheel" - name: List the cuda.core artifacts directory and rename if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }} @@ -709,20 +804,20 @@ jobs: ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }} # Rename wheel to include CUDA version suffix - mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_PREV_CUDA_MAJOR}" + mkdir -p "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${MAINTENANCE_CUDA_MAJOR}" for wheel in ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl; do if [[ -f "${wheel}" ]]; then base_name=$(basename "${wheel}" .whl) - new_name="${base_name}.cu${BUILD_PREV_CUDA_MAJOR}.whl" - mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${BUILD_PREV_CUDA_MAJOR}/${new_name}" + new_name="${base_name}.cu${MAINTENANCE_CUDA_MAJOR}.whl" + mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/cu${MAINTENANCE_CUDA_MAJOR}/${new_name}" echo "Renamed wheel to: ${new_name}" fi done ls -lahR ${{ env.CUDA_CORE_ARTIFACTS_DIR }} - - name: Build maintenance cuda.core Cython tests - if: ${{ !inputs.single-cuda-major && env.TEST_CORE == 'true' }} + - name: Build cuda.core ${{ env.MAINTENANCE_CUDA_VARIANT }} Cython tests + if: ${{ !inputs.single-cuda-major && env.TEST_CORE_MAINTENANCE == 'true' }} run: | # The current package's test extensions were built in this same source tree. # Remove generated outputs so Cython and the compiler cannot reuse them. @@ -748,10 +843,10 @@ jobs: popd - name: Upload maintenance cuda.core Cython tests - if: ${{ !inputs.single-cuda-major && env.TEST_CORE == 'true' }} + if: ${{ !inputs.single-cuda-major && env.TEST_CORE_MAINTENANCE == 'true' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-cu${{ env.MAINTENANCE_CUDA_MAJOR }}-tests + name: ${{ env.CUDA_CORE_ARTIFACT_NAME }}-${{ env.MAINTENANCE_CUDA_VARIANT }}-tests path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }}/test_*${{ env.PY_EXT_SUFFIX }} if-no-files-found: error @@ -760,15 +855,15 @@ jobs: run: | pip install wheel python ci/tools/merge_cuda_core_wheels.py \ - "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${BUILD_CUDA_MAJOR}"/cuda_core*.whl \ - "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${BUILD_PREV_CUDA_MAJOR}"/cuda_core*.whl \ + "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${CURRENT_CUDA_MAJOR}"/cuda_core*.whl \ + "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${MAINTENANCE_CUDA_MAJOR}"/cuda_core*.whl \ --output-dir "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" - name: Finalize single-major cuda.core wheel if: ${{ inputs.single-cuda-major && env.BUILD_CORE == 'true' }} run: | - for wheel in "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${BUILD_CUDA_MAJOR}"/*.cu"${BUILD_CUDA_MAJOR}".whl; do - base_name=$(basename "${wheel}" ".cu${BUILD_CUDA_MAJOR}.whl") + for wheel in "${{ env.CUDA_CORE_ARTIFACTS_DIR }}"/cu"${CURRENT_CUDA_MAJOR}"/*.cu"${CURRENT_CUDA_MAJOR}".whl; do + base_name=$(basename "${wheel}" ".cu${CURRENT_CUDA_MAJOR}.whl") mv "${wheel}" "${{ env.CUDA_CORE_ARTIFACTS_DIR }}/${base_name}.whl" done ls -lahR "${{ env.CUDA_CORE_ARTIFACTS_DIR }}" @@ -779,6 +874,7 @@ jobs: twine check --strict ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl - name: Upload cuda.core build artifacts + if: ${{ env.BUILD_CORE == 'true' || env.BASELINE_RUN_ID != '' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ env.CUDA_CORE_ARTIFACT_NAME }} diff --git a/.github/workflows/ci-pixi-source-test.yml b/.github/workflows/ci-pixi-source-test.yml index 85569908e6e..32caef7520c 100644 --- a/.github/workflows/ci-pixi-source-test.yml +++ b/.github/workflows/ci-pixi-source-test.yml @@ -11,14 +11,14 @@ # cython-test build mechanics drift (see #2182, #2183). This workflow is the # only thing that runs the pixi source build end to end. # -# Two tiers, to spend GPU minutes deliberately (GPUs are scarce): -# - build-smoke (PRs): CPU-only. Source-builds both bindings roots and current -# Core, imports them, builds Cython extensions and checks placement. Catches the +# Three tiers, to spend GPU minutes deliberately (GPUs are scarce): +# - build-smoke (PRs): CPU-only. Source-builds bindings + core, imports them, +# builds the cython test extensions and checks placement. Catches the # compile / ABI / .so-placement regressions WITHOUT a GPU. -# - build-identity-roundtrip (nightly + manual): CPU-only. cu13 -> cu12 -> -# cu13 in one checkout, so build artifacts cannot be reused across CUDA -# majors. Kept off PRs to avoid adding another job to the org's shared -# concurrent-job quota; the fast unit tests in +# - build-identity-roundtrip (nightly + manual): CPU-only. The current ABI -> +# another configured ABI -> current ABI in one checkout, so build artifacts +# cannot be reused across CUDA majors. Kept off PRs to avoid adding another +# job to the org's shared concurrent-job quota; the fast unit tests in # cuda_core/tests/test_build_hooks.py cover the same intent per-PR. # - full-test (nightly + manual): GPU runner, full `pixi run test`. @@ -41,28 +41,25 @@ on: # Path-dependency metadata (including cuda_python_test_helpers) feeds the # solve, so it can break the source build without a pixi.toml edit. - "**/pyproject.toml" - - "cuda_bindings_13/build_hooks.py" - - "cuda_bindings_12/build_hooks.py" + - "cuda_bindings*/build_hooks.py" - "cuda_core/build_hooks.py" - - "cuda_bindings_13/cuda/bindings/**" # generated bindings sources - - "cuda_bindings_12/cuda/bindings/**" - - "cuda_bindings_13/tests/cython/**" - - "cuda_bindings_12/tests/**" + - "cuda_bindings*/cuda/bindings/**" # generated bindings sources + - "cuda_bindings*/docs/**" + - "cuda_bindings*/tests/cython/**" - "cuda_core/tests/cython/**" - - "ci/versions.yml" - - "ci/pyproject.toml" - "ci/tools/bindings_config.py" - - "ci/tools/bindings_scm_version.py" + - "ci/tools/compute_ci_plan.py" + - "ci/pyproject.toml" + - "ci/versions.yml" - "ci/pixi-version.env" - ".github/actions/setup-pixi/action.yml" - ".github/workflows/ci-pixi-source-test.yml" workflow_dispatch: inputs: cuda-env: - description: "pixi environment to test (cu13 or cu12)." - type: choice - options: ["cu13", "cu12"] - default: "cu13" + description: "Registered pixi CUDA ABI environment to test (empty selects current)." + type: string + default: "" defaults: run: @@ -80,92 +77,191 @@ env: jobs: # ── PR guard: CPU-only build + import + placement smoke ── build-smoke: - name: "build smoke (${{ matrix.cuda-env }}, linux-64, CPU)" + name: "build smoke (selected CUDA packages, linux-64, CPU)" if: >- github.repository_owner == 'nvidia' && (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch') runs-on: ubuntu-latest timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - cuda-env: ${{ fromJSON(github.event_name == 'workflow_dispatch' && format('["{0}"]', inputs.cuda-env) || '["cu12","cu13"]') }} - env: - CUDA_ENV: ${{ matrix.cuda-env }} - BINDINGS_ROOT: cuda_bindings_${{ matrix.cuda-env == 'cu12' && '12' || '13' }} steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # Full history + tags so setuptools-scm derives the real (13.x) + # Full history + tags so setuptools-scm derives the real # package version; a shallow checkout yields 0.1.dev1, which trips - # cuda.core's "cuda.bindings 12.x or 13.x must be installed" guard. + # cuda.core's supported cuda.bindings-version guard. fetch-depth: 0 filter: blob:none # third_party/cccl provides the CCCL headers cuda.core compiles against. submodules: true - - name: Set up Python for maintenance preview - if: ${{ matrix.cuda-env == 'cu12' }} + - name: Set up Python uses: ./.github/actions/setup-hardened-python with: python-version: "3.12" - - name: Bootstrap maintenance SCM in this disposable checkout - if: ${{ matrix.cuda-env == 'cu12' }} - env: - SOURCE_SHA: ${{ github.sha }} - run: | - python -m pip install -e ./ci - python -m ci.tools.bindings_scm_version --package-root cuda_bindings_12 --sha "$SOURCE_SHA" + - name: Install CI tool dependencies + run: python3 -m pip install -e ./ci - name: Setup pixi uses: ./.github/actions/setup-pixi - - name: Source-build + import + cython-placement smoke + - name: Select CUDA environments + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || '' }} + EVENT_NAME: ${{ github.event_name }} + REQUESTED_CUDA_ENV: ${{ inputs.cuda-env || '' }} run: | - # pathfinder: pure-Python, no GPU. - pixi run -e "${CUDA_ENV}" test-pathfinder - - # Force each bindings source build (catches nvrtc/driver - # compile errors like #2182) and import them (catches ABI mismatches). - pixi run --manifest-path "${BINDINGS_ROOT}" -e "${CUDA_ENV}" \ - python -c "import cuda.bindings.driver, cuda.bindings.nvrtc, cuda.bindings.runtime; print('bindings import OK')" - if [[ "${CUDA_ENV}" == cu13 ]]; then - pixi run --manifest-path cuda_core -e "${CUDA_ENV}" \ - python -c "import cuda.core; print('core import OK')" + bindings_packages=$(python3 -m ci.tools.bindings_config --package-roots) + selected_packages='[]' + docs_packages='[]' + + merge_packages() { + jq -cn --argjson left "$1" --argjson right "$2" \ + '$left + $right | unique_by(.package_root)' + } + + if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then + if [[ -n "${REQUESTED_CUDA_ENV}" ]]; then + selected_packages=$(jq -c --arg cuda_env "${REQUESTED_CUDA_ENV}" \ + '[.[] | select(.cuda_variant == $cuda_env)]' <<< "${bindings_packages}") + if [[ "${selected_packages}" == '[]' ]]; then + registered_envs=$(jq -r '[.[].cuda_variant] | join(", ")' <<< "${bindings_packages}") + echo "::error::cuda-env must be a registered pixi environment: ${registered_envs}" + exit 1 + fi + else + selected_packages=$(jq -c '[.[] | select(.release_status == "current")]' \ + <<< "${bindings_packages}") + fi + else + merge_base=$(git merge-base HEAD "${BASE_SHA}") + workplan=$(python3 -m ci.tools.compute_ci_plan \ + --merge-base "${merge_base}" \ + --baseline-run-id pixi-source) + + # Keep package-specific work package-specific, while selecting the + # registered package for any ABI-level work. + selected_packages=$(jq -cn \ + --argjson registry "${bindings_packages}" \ + --argjson plan "${workplan}" ' + $registry + | map(. as $package | select( + ($plan.modules.bindings.package_roots[$package.package_root].needs_build // false) + or ($plan.modules.bindings.package_roots[$package.package_root].needs_test // false) + or ($plan.modules.python.package_roots[$package.package_root].needs_build // false) + or ($plan.modules.python.package_roots[$package.package_root].needs_test // false) + or ($plan.jobs.test_cuda_majors[$package.cuda_variant] // false) + )) + ') + + # Pixi manifests are deliberately ignored by the wheel planner. + # Add their package-local impact to its source/test decisions. The + # wheel planner also deliberately ignores documentation changes. + while IFS= read -r -d '' path; do + manifest_packages=$(jq -c --arg path "${path}" ' + [.[] as $package + | select( + $path == ($package.package_root + "/pixi.toml") + or $path == ($package.package_root + "/pixi.lock") + ) + | $package] + ' <<< "${bindings_packages}") + if [[ "${manifest_packages}" != '[]' ]]; then + selected_packages=$(merge_packages "${selected_packages}" "${manifest_packages}") + else + case "${path}" in + */pixi.toml|*/pixi.lock|pixi.toml|pixi.lock) + selected_packages=$(merge_packages "${selected_packages}" "${bindings_packages}") + ;; + esac + fi + + changed_docs_packages=$(jq -c --arg path "${path}" ' + [.[] as $package + | select($path | startswith($package.package_root + "/docs/")) + | $package] + ' <<< "${bindings_packages}") + if [[ "${changed_docs_packages}" != '[]' ]]; then + selected_packages=$(merge_packages "${selected_packages}" "${changed_docs_packages}") + docs_packages=$(merge_packages "${docs_packages}" "${changed_docs_packages}") + fi + done < <(git diff --no-renames --name-only -z "${merge_base}" HEAD) fi - # cython test extensions: build them and confirm each .so landed next - # to its .pyx in tests/cython (catches the placement regression #2180). - pixi run --manifest-path "${BINDINGS_ROOT}" -e "${CUDA_ENV}" build-cython-tests - cython_dirs=("${BINDINGS_ROOT}/tests/cython") - if [[ "${CUDA_ENV}" == cu13 ]]; then - pixi run --manifest-path cuda_core -e "${CUDA_ENV}" build-cython-tests - cython_dirs+=(cuda_core/tests/cython) + if [[ "${selected_packages}" == '[]' ]]; then + echo "::error::no bindings package selected for source-build smoke tests" + exit 1 fi - for d in "${cython_dirs[@]}"; do - if ! compgen -G "${d}/*.cpython-*.so" > /dev/null; then - echo "::error::no compiled cython test .so in ${d} (placement regression)" - exit 1 + echo "BINDINGS_PACKAGES=${selected_packages}" | tee -a "$GITHUB_ENV" + { + echo '### Selected source builds' + jq -r '.[] | "- \(.package_root): CUDA \(.cuda_major), pixi \(.cuda_variant)"' <<< "$selected_packages" + } >> "$GITHUB_STEP_SUMMARY" + echo "BINDINGS_DOC_PACKAGES=${docs_packages}" | tee -a "$GITHUB_ENV" + + - name: Source-build + import + cython-placement smoke + run: | + while IFS= read -r bindings_package; do + BINDINGS_ROOT=$(jq -r '.package_root' <<< "${bindings_package}") + CUDA_ENV=$(jq -r '.cuda_variant' <<< "${bindings_package}") + echo "::group::Source-build ${BINDINGS_ROOT} (${CUDA_ENV})" + if [[ $(jq -r '.release_status' <<< "${bindings_package}") == maintenance ]]; then + python3 -m ci.tools.bindings_scm_version --package-root "${BINDINGS_ROOT}" --sha "$GITHUB_SHA" fi - done + # Avoid carrying Cython outputs between bindings packages or CUDA ABIs. + rm -rf cuda_core/build/cython cuda_core/cython_debug + find cuda_core/cuda/core -type f -name '*.so' -delete + find "${BINDINGS_ROOT}/tests/cython" cuda_core/tests/cython \ + -maxdepth 1 -type f \( -name 'test_*.cpp' -o -name 'test_*.so' \) -delete + + # pathfinder: pure-Python, no GPU. + pixi run -e "${CUDA_ENV}" test-pathfinder + + # Force both source builds and import them to catch compile/ABI + # regressions such as #2182. cuda-core environments may use + # published bindings by design; wheel CI covers local pairing. + pixi run --manifest-path "${BINDINGS_ROOT}" -e "${CUDA_ENV}" \ + python -c "import cuda.bindings.driver, cuda.bindings.nvrtc, cuda.bindings.runtime; print('bindings import OK')" + pixi run --manifest-path cuda_core -e "${CUDA_ENV}" \ + python -c "import cuda.core; print('core import OK')" + pixi run --manifest-path "${BINDINGS_ROOT}" -e "${CUDA_ENV}" build-cython-tests + pixi run --manifest-path cuda_core -e "${CUDA_ENV}" build-cython-tests + + # Confirm each extension landed next to its .pyx (catches #2180). + for d in "${BINDINGS_ROOT}/tests/cython" cuda_core/tests/cython; do + if ! compgen -G "${d}/*.cpython-*.so" > /dev/null; then + echo "::error::no compiled cython test .so in ${d} (placement regression)" + exit 1 + fi + done + echo "::endgroup::" + done < <(jq -c '.[]' <<< "${BINDINGS_PACKAGES}") echo "cython test extensions placed correctly" + - name: Build changed bindings documentation + if: ${{ env.BINDINGS_DOC_PACKAGES != '[]' }} + run: | + while IFS= read -r bindings_package; do + bindings_root=$(jq -r '.package_root' <<< "${bindings_package}") + pixi run --manifest-path "${bindings_root}" -e docs build-docs + done < <(jq -c '.[]' <<< "${BINDINGS_DOC_PACKAGES}") + # ── Nightly guard: build artifacts must be CUDA-major aware ── # # Neither Cython nor setuptools tracks the CUDA major in its own up-to-date # check: Cython does not hash `compile_time_env`, and an editable install's # .so is named by the Python ABI tag alone. Before build_hooks keyed them, - # a cu13 build followed by a cu12 build in the same checkout failed while - # compiling cu13-generated C++ against CUDA 12 headers. + # a build for one ABI followed by another in the same checkout failed while + # compiling generated C++ against headers from the second ABI. # - # The round trip (not just cu13 -> cu12) is what catches the second half: - # coming back to cu13 must not silently reuse the cu12 extension. + # The round trip is what catches the second half: coming back to the current + # ABI must not silently reuse the other ABI's extension. # - # Core only: bindings majors have separate source roots and environments. + # cuda_core only: not every bindings package can be source-built in every ABI + # environment, for reasons unrelated to stale artifacts. build-identity-roundtrip: - name: "cu13 -> cu12 -> cu13 round trip (linux-64, CPU)" + name: "current -> maintenance -> current round trip (linux-64, CPU)" if: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.repository_owner == 'nvidia' }} runs-on: ubuntu-latest timeout-minutes: 60 @@ -173,26 +269,41 @@ jobs: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # Full history + tags so setuptools-scm derives the real (13.x) + # Full history + tags so setuptools-scm derives the real # package version; a shallow checkout yields 0.1.dev1, which trips - # cuda.core's "cuda.bindings 12.x or 13.x must be installed" guard. + # cuda.core's supported cuda.bindings-version guard. fetch-depth: 0 filter: blob:none # third_party/cccl provides the CCCL headers cuda.core compiles against. submodules: true + - name: Set up Python + uses: ./.github/actions/setup-hardened-python + with: + python-version: "3.12" + + - name: Install CI tool dependencies + run: python3 -m pip install -e ./ci + - name: Setup pixi uses: ./.github/actions/setup-pixi - - name: Build cu13, then cu12, then cu13 again in one checkout + - name: Build current, another ABI, then current again in one checkout run: | - for cuda_env in cu13 cu12 cu13; do + current_package=$(python3 -m ci.tools.bindings_config --release-status current) + bindings_packages=$(python3 -m ci.tools.bindings_config --package-roots) + current_env=$(jq -r '.cuda_variant' <<< "${current_package}") + other_env=$(jq -r --arg current_env "${current_env}" \ + '[.[] | select(.cuda_variant != $current_env) | .cuda_variant] | first // empty' \ + <<< "${bindings_packages}") + + for cuda_env in "${current_env}" "${other_env}" "${current_env}"; do echo "::group::${cuda_env}" pixi run --manifest-path cuda_core -e "${cuda_env}" \ python -c "import cuda.core; print('core import OK')" echo "::endgroup::" done - # The last build was cu13, and each major must have kept its own + # The last build was current, and each ABI must have kept its own # generated sources rather than overwriting the other's. # Stamp is ABI-scoped (.build-config.). Unset and gnu # both record as "gnu"; llvm records as "llvm". @@ -203,28 +314,24 @@ jobs: exit 1 fi stamp=$(<"${stamps[0]}") - if [[ ! "${stamp}" =~ ^cu13-(gnu|llvm)-(debug|opt)(-cov)?$ ]]; then - echo "::error::build stamp is '${stamp}', expected cu13-{gnu|llvm}-{debug|opt}" + if [[ ! "${stamp}" =~ ^${current_env}-(gnu|llvm)-(debug|opt)(-cov)?$ ]]; then + echo "::error::build stamp is '${stamp}', expected ${current_env}-{gnu|llvm}-{debug|opt}" exit 1 fi - suffix="${stamp#cu13-}" - for major in cu12 cu13; do - if [ ! -d "cuda_core/build/cython/${major}-${suffix}" ]; then - echo "::error::no ${major}-${suffix} generated-source directory" + suffix="${stamp#"${current_env}-"}" + for cuda_env in "${other_env}" "${current_env}"; do + if [ ! -d "cuda_core/build/cython/${cuda_env}-${suffix}" ]; then + echo "::error::no ${cuda_env}-${suffix} generated-source directory" exit 1 fi done # ── Nightly: full `pixi run test` on a GPU runner ── full-test: - name: "pixi run test (${{ matrix.cuda-env }}, linux-64, GPU)" + name: "pixi run test (${{ inputs.cuda-env || 'current' }}, linux-64, GPU)" if: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.repository_owner == 'nvidia' }} runs-on: "linux-amd64-gpu-l4-latest-1" # same label scheme as test-wheel-linux.yml timeout-minutes: 90 - strategy: - fail-fast: false - matrix: - cuda-env: ${{ fromJSON(github.event_name == 'workflow_dispatch' && format('["{0}"]', inputs.cuda-env) || '["cu12","cu13"]') }} container: options: -u root --security-opt seccomp=unconfined --shm-size 16g image: ubuntu:24.04 @@ -237,17 +344,17 @@ jobs: - name: Install system packages run: | apt-get update - # ca-certificates + git for checkout; libgl1/libegl1 for pyglet - # (pulled in by the test env). pixi itself is installed by setup-pixi. + # ca-certificates + git for checkout; libgl1/libegl1 for pyglet. + # The hardened Python setup below supplies the CI helper interpreter. apt-get install -y --no-install-recommends \ - ca-certificates git libgl1 libegl1 + ca-certificates git jq libgl1 libegl1 - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # Full history + tags so setuptools-scm derives the real (13.x) + # Full history + tags so setuptools-scm derives the real # package version; a shallow checkout yields 0.1.dev1, which trips - # cuda.core's "cuda.bindings 12.x or 13.x must be installed" guard. + # cuda.core's supported cuda.bindings-version guard. fetch-depth: 0 filter: blob:none # third_party/cccl provides the CCCL headers cuda.core compiles against. @@ -257,28 +364,44 @@ jobs: # checkout's temporary Git configuration does not reach later steps. run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Setup proxy cache - uses: nv-gha-runners/setup-proxy-cache@main - continue-on-error: true - - - name: Set up Python for maintenance preview - if: ${{ matrix.cuda-env == 'cu12' }} + - name: Set up Python uses: ./.github/actions/setup-hardened-python with: python-version: "3.12" - - name: Bootstrap maintenance SCM in this disposable checkout - if: ${{ matrix.cuda-env == 'cu12' }} + - name: Install CI tool dependencies + run: python3 -m pip install -e ./ci + + - name: Prepare selected CUDA environment env: - SOURCE_SHA: ${{ github.sha }} + REQUESTED_CUDA_ENV: ${{ inputs.cuda-env || '' }} run: | - python -m pip install -e ./ci - python -m ci.tools.bindings_scm_version --package-root cuda_bindings_12 --sha "$SOURCE_SHA" + if [[ -n "${REQUESTED_CUDA_ENV}" ]]; then + bindings_packages=$(python3 -m ci.tools.bindings_config --package-roots) + bindings_package=$(jq -c --arg cuda_env "${REQUESTED_CUDA_ENV}" \ + '.[] | select(.cuda_variant == $cuda_env)' <<< "${bindings_packages}") + if [[ -z "${bindings_package}" ]]; then + registered_envs=$(jq -r '[.[].cuda_variant] | join(", ")' <<< "${bindings_packages}") + echo "::error::cuda-env must be a registered pixi environment: ${registered_envs}" + exit 1 + fi + else + bindings_package=$(python3 -m ci.tools.bindings_config --release-status current) + fi + cuda_env=$(jq -r '.cuda_variant' <<< "${bindings_package}") + if [[ $(jq -r '.release_status' <<< "${bindings_package}") == maintenance ]]; then + bindings_root=$(jq -r '.package_root' <<< "${bindings_package}") + # Match build-smoke's version preparation before any pixi source build. + python3 -m ci.tools.bindings_scm_version --package-root "${bindings_root}" --sha "$GITHUB_SHA" + fi + echo "CUDA_ENV=${cuda_env}" | tee -a "$GITHUB_ENV" + + - name: Setup proxy cache + uses: nv-gha-runners/setup-proxy-cache@main + continue-on-error: true - name: Setup pixi uses: ./.github/actions/setup-pixi - name: pixi run test - env: - CUDA_ENV: ${{ matrix.cuda-env }} run: pixi run -e "${CUDA_ENV}" test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e970cd1e95d..e917a3522d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,6 @@ # SPDX-License-Identifier: Apache-2.0 # Note: This name is referred to in the test job, so make sure any changes are sync'd up! -# Further this is referencing a run in the backport branch to fetch old bindings. name: "CI" concurrency: @@ -27,70 +26,6 @@ on: workflow_dispatch: {} jobs: - ci-vars: - if: ${{ github.repository_owner == 'nvidia' }} - runs-on: ubuntu-latest - outputs: - CUDA_BUILD_VER: ${{ steps.get-vars.outputs.cuda_build_ver }} - CUDA_PREV_BUILD_VER: ${{ steps.get-vars.outputs.cuda_prev_build_ver }} - WINDOWS_ARM64_SUPPORTED: ${{ steps.get-vars.outputs.windows_arm64_supported }} - WINDOWS_ARM64_SINGLE_CUDA_MAJOR: ${{ steps.get-vars.outputs.windows_arm64_single_cuda_major }} - PYTHON_SUPPORTED: ${{ steps.get-vars.outputs.python_supported }} - BINDINGS_CONFIG: ${{ steps.get-vars.outputs.bindings_config }} - steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 1 - - name: Set up Python for CI tools - uses: ./.github/actions/setup-hardened-python - with: - python-version: "3.12" - - name: Install CI tool dependencies - run: python -m pip install -e ./ci - - name: Get CUDA build versions - id: get-vars - run: | - bindings_config=$(python -m ci.tools.bindings_config) - echo "bindings_config=$bindings_config" >> "$GITHUB_OUTPUT" - cuda_build_ver=$(yq '.cuda.build.version' ci/versions.yml) - echo "cuda_build_ver=$cuda_build_ver" >> $GITHUB_OUTPUT - - cuda_prev_build_ver=$(yq '.cuda.prev_build.version' ci/versions.yml) - echo "cuda_prev_build_ver=$cuda_prev_build_ver" >> $GITHUB_OUTPUT - - # Flattened one-line JSON for pass-through to build-wheel.yml via - # fromJSON on the receiving side. - python_supported=$(yq -o json -I 0 '.python.supported' ci/versions.yml) - echo "python_supported=$python_supported" >> $GITHUB_OUTPUT - - # Windows ARM64 is available starting with CUDA 13.4. - if [[ "$cuda_build_ver" =~ ^([0-9]+)\.([0-9]+)(\.|$) ]]; then - cuda_build_major="${BASH_REMATCH[1]}" - cuda_build_minor="${BASH_REMATCH[2]}" - else - echo "Invalid CUDA build version: $cuda_build_ver" >&2 - exit 1 - fi - if (( cuda_build_major > 13 || (cuda_build_major == 13 && cuda_build_minor >= 4) )); then - windows_arm64_supported=true - else - windows_arm64_supported=false - fi - echo "windows_arm64_supported=$windows_arm64_supported" >> $GITHUB_OUTPUT - - # No CUDA 13 windows-arm64 toolkit exists for a major other than the - # current one (windows-arm64 support started mid-way through the 13.x - # series), so cuda.core can only be built against a single CUDA major - # while the build major is still 13. Once the build major advances to - # 14, a CUDA 13 windows-arm64 toolkit will exist as the prior major. - if [[ "$cuda_build_major" == "13" ]]; then - windows_arm64_single_cuda_major=true - else - windows_arm64_single_cuda_major=false - fi - echo "windows_arm64_single_cuda_major=$windows_arm64_single_cuda_major" >> $GITHUB_OUTPUT - should-skip: if: ${{ github.repository_owner == 'nvidia' }} runs-on: ubuntu-latest @@ -131,19 +66,22 @@ jobs: # # Dependency graph (verified in pyproject.toml files): # cuda_pathfinder -> (no internal deps) - # cuda_bindings -> cuda_pathfinder + # cuda.bindings (both versions) -> cuda_pathfinder # cuda_core -> cuda_pathfinder, cuda_bindings # cuda_python -> cuda_pathfinder, cuda_bindings, cuda_core (meta package) # # A change to cuda_pathfinder (or shared infra) forces a rebuild of every - # downstream module. A change to cuda_bindings forces rebuild of cuda_core. + # downstream module. A bindings-only change rebuilds its bindings and + # metapackage, then tests them with the unchanged Core wheel from the baseline. + # Core source changes rebuild both CUDA variants. # A change to cuda_core alone skips rebuilding/retesting cuda_bindings and # cuda_pathfinder, but still retests the downstream cuda-python metapackage. # Shared build/orchestration changes run the full pipeline; test-only CI # infrastructure runs every test suite without rebuilding package wheels. - # On push to main, tag refs, schedule, or workflow_dispatch events we - # unconditionally run everything because there is no meaningful "changed - # paths" baseline for those events. + # On pushes to main, schedule, workflow_dispatch, or non-bindings release + # tags we unconditionally run everything because there is no meaningful + # "changed paths" baseline for those events. Bare bindings tags select the + # corresponding registered cuda.bindings package root and cuda-python build. detect-changes: if: ${{ github.repository_owner == 'nvidia' }} runs-on: ubuntu-latest @@ -152,6 +90,11 @@ jobs: actions: read contents: read outputs: + bindings-config: ${{ steps.registry.outputs.bindings-config }} + current-cuda-version: ${{ steps.registry.outputs.current-cuda-version }} + python-supported: ${{ steps.registry.outputs.python-supported }} + windows-arm64-supported: ${{ steps.registry.outputs.windows-arm64-supported }} + windows-arm64-single-cuda-major: ${{ steps.registry.outputs.windows-arm64-single-cuda-major }} workplan: ${{ steps.workplan.outputs.workplan }} steps: - name: Checkout repository @@ -161,6 +104,56 @@ jobs: fetch-depth: ${{ startsWith(github.ref_name, 'pull-request/') && '0' || '1' }} filter: blob:none + - name: Set up Python for CI tools + uses: ./.github/actions/setup-hardened-python + with: + python-version: "3.12" + + - name: Install CI tool test dependencies + run: python3 -m pip install -e './ci[test]' + + - name: Read bindings configuration and select supported build platforms + id: registry + run: | + set -euo pipefail + bindings_config=$(python3 -m ci.tools.bindings_config) + current_cuda_version=$(jq -er \ + '.package_roots[] | select(.release_status == "current") | .toolkit_version' \ + <<< "$bindings_config") + + if [[ "$current_cuda_version" =~ ^([0-9]+)\.([0-9]+)(\.|$) ]]; then + current_cuda_major="${BASH_REMATCH[1]}" + current_cuda_minor="${BASH_REMATCH[2]}" + else + echo "Invalid current CUDA version: $current_cuda_version" >&2 + exit 1 + fi + + # Windows ARM64 is available starting with CUDA 13.4. + if (( current_cuda_major > 13 || (current_cuda_major == 13 && current_cuda_minor >= 4) )); then + windows_arm64_supported=true + else + windows_arm64_supported=false + fi + + # CUDA 13 introduced Windows ARM64 mid-release-line, so there is no + # CUDA 12 Windows ARM64 toolkit. Once current advances to CUDA 14, + # CUDA 13 can be built as the maintenance major on this platform; + # keep this equality so CUDA 14 does not exclude that supported line. + if [[ "$current_cuda_major" == "13" ]]; then + windows_arm64_single_cuda_major=true + else + windows_arm64_single_cuda_major=false + fi + + { + echo "python-supported=$(yq -o json -I 0 '.python.supported' ci/versions.yml)" + echo "bindings-config=$bindings_config" + echo "current-cuda-version=$current_cuda_version" + echo "windows-arm64-supported=$windows_arm64_supported" + echo "windows-arm64-single-cuda-major=$windows_arm64_single_cuda_major" + } >> "$GITHUB_OUTPUT" + - name: Resolve PR merge base id: merge-base if: ${{ startsWith(github.ref_name, 'pull-request/') }} @@ -181,8 +174,12 @@ jobs: if: ${{ startsWith(github.ref_name, 'pull-request/') }} env: BASE_REF: ${{ needs.should-skip.outputs.base-ref }} + BINDINGS_CONFIG: ${{ steps.registry.outputs.bindings-config }} + CURRENT_CUDA_VERSION: ${{ steps.registry.outputs.current-cuda-version }} MERGE_BASE: ${{ steps.merge-base.outputs.sha }} GH_TOKEN: ${{ github.token }} + WINDOWS_ARM64_SUPPORTED: ${{ steps.registry.outputs.windows-arm64-supported }} + WINDOWS_ARM64_SINGLE_CUDA_MAJOR: ${{ steps.registry.outputs.windows-arm64-single-cuda-major }} run: | set -uo pipefail @@ -232,26 +229,42 @@ jobs: } missing=() - for name in cuda-pathfinder-wheel cuda-python-wheel; do + has_artifact cuda-pathfinder-wheel || missing+=(cuda-pathfinder-wheel) + + if ! cuda_versions_json=$(jq -ce '[.package_roots[].toolkit_version]' <<< "$BINDINGS_CONFIG"); then + unavailable + fi + mapfile -t cuda_versions < <(jq -r '.[]' <<< "$cuda_versions_json") + for version in "${cuda_versions[@]}"; do + name="cuda-python-wheel-cuda${version}" has_artifact "$name" || missing+=("$name") done - cuda_version=$(yq '.cuda.build.version' ci/versions.yml) if ! python_versions=$(yq -r '.python.supported[]' ci/versions.yml); then unavailable fi if ! platforms=$(yq -r '.platforms[]' ci/test-matrix.yml); then unavailable fi + if [[ "$WINDOWS_ARM64_SUPPORTED" == "true" ]]; then + platforms+=$'\nwin-arm64' + fi if [[ -z "${python_versions}" || -z "${platforms}" ]]; then unavailable fi while IFS= read -r python_version; do python=${python_version//./} while IFS= read -r platform; do - binding="cuda-bindings-python${python}-cuda${cuda_version}-${platform}-${merge_base}" + for version in "${cuda_versions[@]}"; do + if [[ "$platform" == "win-arm64" && + "$WINDOWS_ARM64_SINGLE_CUDA_MAJOR" == "true" && + "$version" != "$CURRENT_CUDA_VERSION" ]]; then + continue + fi + binding="cuda-bindings-python${python}-cuda${version}-${platform}-${merge_base}" + has_artifact "$binding" || missing+=("$binding") + done core="cuda-core-python${python}-${platform}-${merge_base}" - has_artifact "$binding" || missing+=("$binding") has_artifact "$core" || missing+=("$core") done <<< "${platforms}" done <<< "${python_versions}" @@ -267,34 +280,28 @@ jobs: echo "Reusable artifacts: run \`${run_id}\` at \`${merge_base}\` on \`${BASE_REF}\`." } >> "$GITHUB_STEP_SUMMARY" - - name: Set up Python for CI tool tests - uses: ./.github/actions/setup-hardened-python - with: - python-version: "3.12" - - name: Test CI tools run: | - python -m pip install -e './ci[test]' - python -m pytest -v --noconftest ci/tools/tests + python3 -m pytest -q --noconftest ci/tools/tests - name: Compute CI workplan id: workplan env: MERGE_BASE: ${{ steps.merge-base.outputs.sha }} BASELINE_RUN_ID: ${{ steps.baseline.outputs.run_id }} + RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }} run: | set -euo pipefail - workplan=$(python3 ci/tools/compute_ci_plan.py \ - --merge-base "$MERGE_BASE" \ - --baseline-run-id "$BASELINE_RUN_ID") - echo "workplan=$workplan" >> "$GITHUB_OUTPUT" - { - echo - echo "### CI workplan" - echo '```json' - jq . <<< "$workplan" - echo '```' - } >> "$GITHUB_STEP_SUMMARY" + planner_args=( + --merge-base "$MERGE_BASE" + --baseline-run-id "$BASELINE_RUN_ID" + ) + if [[ -n "$RELEASE_TAG" ]]; then + planner_args+=(--release-tag "$RELEASE_TAG") + fi + python3 -m ci.tools.compute_ci_plan "${planner_args[@]}" \ + --github-output "$GITHUB_OUTPUT" \ + --github-step-summary "$GITHUB_STEP_SUMMARY" api-check-core-vs-release: name: API check (cuda_core vs. latest release) @@ -388,7 +395,6 @@ jobs: # - if: condition (build-linux-64 omits doc-only check since it's needed for docs) build-linux-64: needs: - - ci-vars - should-skip - detect-changes strategy: @@ -396,7 +402,7 @@ jobs: matrix: host-platform: - linux-64 - name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + name: Build (${{ matrix.host-platform }}) if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) }} permissions: actions: read @@ -405,15 +411,13 @@ jobs: uses: ./.github/workflows/build-wheel.yml with: host-platform: ${{ matrix.host-platform }} - cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + bindings-config: ${{ needs.detect-changes.outputs.bindings-config }} workplan: ${{ needs.detect-changes.outputs.workplan }} - python-versions: ${{ needs.ci-vars.outputs.PYTHON_SUPPORTED }} + python-versions: ${{ needs.detect-changes.outputs.python-supported }} # See build-linux-64 for why build jobs are split by platform. build-linux-aarch64: needs: - - ci-vars - should-skip - detect-changes strategy: @@ -421,7 +425,7 @@ jobs: matrix: host-platform: - linux-aarch64 - name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + name: Build (${{ matrix.host-platform }}) if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) && @@ -433,15 +437,13 @@ jobs: uses: ./.github/workflows/build-wheel.yml with: host-platform: ${{ matrix.host-platform }} - cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + bindings-config: ${{ needs.detect-changes.outputs.bindings-config }} workplan: ${{ needs.detect-changes.outputs.workplan }} - python-versions: ${{ needs.ci-vars.outputs.PYTHON_SUPPORTED }} + python-versions: ${{ needs.detect-changes.outputs.python-supported }} # See build-linux-64 for why build jobs are split by platform. build-windows: needs: - - ci-vars - should-skip - detect-changes strategy: @@ -449,7 +451,7 @@ jobs: matrix: host-platform: - win-64 - name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + name: Build (${{ matrix.host-platform }}) if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) && @@ -461,23 +463,22 @@ jobs: uses: ./.github/workflows/build-wheel.yml with: host-platform: ${{ matrix.host-platform }} - cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + bindings-config: ${{ needs.detect-changes.outputs.bindings-config }} workplan: ${{ needs.detect-changes.outputs.workplan }} - python-versions: ${{ needs.ci-vars.outputs.PYTHON_SUPPORTED }} + python-versions: ${{ needs.detect-changes.outputs.python-supported }} # Windows ARM64 is available starting with CUDA 13.4. Build only the current # CUDA major because no prior-major toolkit or wheel artifacts exist. build-windows-arm64: needs: - - ci-vars - should-skip - detect-changes - name: Build win-arm64, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} + name: Build (win-arm64) if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) && - needs.ci-vars.outputs.WINDOWS_ARM64_SUPPORTED == 'true' && + needs.detect-changes.outputs.windows-arm64-supported == 'true' && + fromJSON(needs.detect-changes.outputs.workplan).jobs.windows_arm64_build && fromJSON(needs.detect-changes.outputs.workplan).jobs.platforms.windows }} permissions: actions: read @@ -486,11 +487,10 @@ jobs: uses: ./.github/workflows/build-wheel.yml with: host-platform: win-arm64 - cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }} + bindings-config: ${{ needs.detect-changes.outputs.bindings-config }} workplan: ${{ needs.detect-changes.outputs.workplan }} - single-cuda-major: ${{ needs.ci-vars.outputs.WINDOWS_ARM64_SINGLE_CUDA_MAJOR == 'true' }} - python-versions: ${{ needs.ci-vars.outputs.PYTHON_SUPPORTED }} + single-cuda-major: ${{ needs.detect-changes.outputs.windows-arm64-single-cuda-major == 'true' }} + python-versions: ${{ needs.detect-changes.outputs.python-supported }} # NOTE: test-sdist jobs are split by platform (mirroring build-* and test-wheel-*) # so platform-specific sources (e.g. cuda_bindings_13/*_windows.pyx selected by @@ -502,13 +502,12 @@ jobs: strategy: fail-fast: false matrix: - package: ${{ fromJSON(needs.ci-vars.outputs.BINDINGS_CONFIG).package_roots }} + package: ${{ fromJSON(needs.detect-changes.outputs.bindings-config).package_roots }} needs: - - ci-vars - should-skip - detect-changes - build-linux-64 - name: Test sdist linux-64 + name: Sdist ${{ matrix.package.package_root }} (linux-64) if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) && @@ -528,14 +527,13 @@ jobs: strategy: fail-fast: false matrix: - package: ${{ fromJSON(needs.ci-vars.outputs.BINDINGS_CONFIG).package_roots }} + package: ${{ fromJSON(needs.detect-changes.outputs.bindings-config).package_roots }} needs: - - ci-vars - should-skip - detect-changes - build-linux-64 - build-windows - name: Test sdist win-64 + name: Sdist ${{ matrix.package.package_root }} (win-64) if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) && @@ -569,17 +567,15 @@ jobs: actions: read contents: read # This is required for actions/checkout needs: - - ci-vars - should-skip - detect-changes - build-linux-64 secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: ${{ startsWith(github.ref_name, 'pull-request/') && 'pull-request' || (github.event_name == 'schedule' && 'nightly' || 'branch') }} + build-type: pull-request host-platform: ${{ matrix.host-platform }} - build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - bindings-config: ${{ needs.ci-vars.outputs.BINDINGS_CONFIG }} + bindings-config: ${{ needs.detect-changes.outputs.bindings-config }} nruns: ${{ (github.event_name == 'schedule' && 5) || 1}} workplan: ${{ needs.detect-changes.outputs.workplan }} @@ -598,7 +594,6 @@ jobs: actions: read contents: read # This is required for actions/checkout needs: - - ci-vars - should-skip - detect-changes - build-linux-64 @@ -606,10 +601,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml with: - build-type: ${{ startsWith(github.ref_name, 'pull-request/') && 'pull-request' || (github.event_name == 'schedule' && 'nightly' || 'branch') }} + build-type: pull-request host-platform: ${{ matrix.host-platform }} - build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - bindings-config: ${{ needs.ci-vars.outputs.BINDINGS_CONFIG }} + bindings-config: ${{ needs.detect-changes.outputs.bindings-config }} nruns: ${{ (github.event_name == 'schedule' && 5) || 1}} workplan: ${{ needs.detect-changes.outputs.workplan }} @@ -628,7 +622,6 @@ jobs: actions: read contents: read # This is required for actions/checkout needs: - - ci-vars - should-skip - detect-changes - build-linux-64 @@ -636,10 +629,9 @@ jobs: secrets: inherit uses: ./.github/workflows/test-wheel-windows.yml with: - build-type: ${{ startsWith(github.ref_name, 'pull-request/') && 'pull-request' || (github.event_name == 'schedule' && 'nightly' || 'branch') }} + build-type: pull-request host-platform: ${{ matrix.host-platform }} - build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }} - bindings-config: ${{ needs.ci-vars.outputs.BINDINGS_CONFIG }} + bindings-config: ${{ needs.detect-changes.outputs.bindings-config }} nruns: ${{ (github.event_name == 'schedule' && 5) || 1}} workplan: ${{ needs.detect-changes.outputs.workplan }} @@ -652,11 +644,13 @@ jobs: contents: write pull-requests: write needs: - - ci-vars - build-linux-64 secrets: inherit uses: ./.github/workflows/build-docs.yml with: + # Bare cuda.bindings tags intentionally produce only their matching + # cuda.bindings and cuda-python artifacts. Keep tag docs on that focused + # artifact set; component-specific release runs build the other docs. component: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && 'cuda-bindings' || 'all' }} git-tag: ${{ github.ref_type == 'tag' && github.ref_name || '' }} run-id: ${{ github.run_id }} @@ -668,7 +662,6 @@ jobs: if: ${{ always() && github.repository_owner == 'nvidia' }} runs-on: ubuntu-latest needs: - - ci-vars - should-skip - detect-changes - build-linux-64 @@ -705,7 +698,8 @@ jobs: doc_only="${{ needs.should-skip.outputs.doc-only }}" linux_selected="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.platforms.linux || false }}" windows_selected="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.platforms.windows || false }}" - windows_arm64_supported="${{ needs.ci-vars.outputs.WINDOWS_ARM64_SUPPORTED }}" + windows_arm64_supported="${{ needs.detect-changes.outputs.windows-arm64-supported }}" + windows_arm64_selected="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.windows_arm64_build || false }}" build_selected="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.sdist_tests || false }}" run_core_api_check="${{ needs.detect-changes.outputs.workplan && fromJSON(needs.detect-changes.outputs.workplan).jobs.core_api_checks || false }}" is_pr="${{ startsWith(github.ref_name, 'pull-request/') }}" @@ -721,7 +715,6 @@ jobs: } # Control jobs, the universal Linux build, and docs always run. - check_result "ci-vars" "success" check_result "should-skip" "success" check_result "detect-changes" "success" check_result "build-linux-64" "success" @@ -740,7 +733,7 @@ jobs: check_result "build-windows" "$windows_expected" windows_arm64_expected="skipped" - if [[ "$windows_arm64_supported" == "true" ]]; then + if [[ "$windows_arm64_supported" == "true" && "$windows_arm64_selected" == "true" ]]; then windows_arm64_expected="$windows_expected" fi check_result "build-windows-arm64" "$windows_arm64_expected" diff --git a/.github/workflows/test-sdist-linux.yml b/.github/workflows/test-sdist-linux.yml index fa9966beaa5..8b3cdb6e467 100644 --- a/.github/workflows/test-sdist-linux.yml +++ b/.github/workflows/test-sdist-linux.yml @@ -29,14 +29,15 @@ permissions: jobs: test-sdist: name: Test sdist builds - if: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).jobs.sdist_tests }} + if: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).jobs.sdist_package_roots[fromJSON(inputs.bindings-package).package_root] }} env: BINDINGS_ROOT: ${{ fromJSON(inputs.bindings-package).package_root }} CUDA_PYTHON_BUILD_MAJOR: ${{ fromJSON(inputs.bindings-package).cuda_major }} BUILD_PATHFINDER: ${{ fromJSON(inputs.bindings-package).release_status == 'current' && (inputs.workplan == '' || fromJSON(inputs.workplan).modules.pathfinder.needs_build) }} - BUILD_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_build }} - BUILD_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_build }} - BUILD_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.needs_build }} + BUILD_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.package_roots[fromJSON(inputs.bindings-package).package_root].needs_build }} + BUILD_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.cuda_majors[fromJSON(inputs.bindings-package).cuda_variant].needs_build }} + BUILD_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.package_roots[fromJSON(inputs.bindings-package).package_root].needs_build }} + PATHFINDER_SOURCE: ${{ inputs.workplan == '' && 'artifact' || fromJSON(inputs.workplan).sources.pathfinder }} timeout-minutes: 60 runs-on: linux-amd64-cpu8 steps: @@ -77,7 +78,8 @@ jobs: pip wheel --no-deps --wheel-dir cuda_python/dist cuda_python/dist/*.tar.gz - name: Download cuda.pathfinder wheel - if: ${{ env.BUILD_PATHFINDER != 'true' && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') }} + if: ${{ env.BUILD_PATHFINDER != 'true' && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') && + env.PATHFINDER_SOURCE == 'artifact' }} uses: ./.github/actions/download-artifact-retry with: name: cuda-pathfinder-wheel @@ -86,12 +88,19 @@ jobs: - name: Constrain builds to the local cuda.pathfinder wheel if: ${{ env.BUILD_BINDINGS == 'true' }} run: | - pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" mkdir -p wheel-constraints - pathfinder_uri="file://$(realpath "${pathfinder_wheels[0]}")" - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" | tee wheel-constraints/cuda-bindings.txt + if [[ "${PATHFINDER_SOURCE}" == artifact ]]; then + pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl) + test "${#pathfinder_wheels[@]}" -eq 1 + test -f "${pathfinder_wheels[0]}" + pathfinder_uri="file://$(realpath "${pathfinder_wheels[0]}")" + printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" > wheel-constraints/cuda-bindings.txt + elif [[ "${PATHFINDER_SOURCE}" == published ]]; then + : > wheel-constraints/cuda-bindings.txt + else + echo "Error: PATHFINDER_SOURCE must be artifact or published" >&2 + exit 1 + fi # Cython packages need CTK + sccache (local disk cache only in this job). - name: Install rapidsai/sccache diff --git a/.github/workflows/test-sdist-windows.yml b/.github/workflows/test-sdist-windows.yml index 93d06df88db..f9776173ffe 100644 --- a/.github/workflows/test-sdist-windows.yml +++ b/.github/workflows/test-sdist-windows.yml @@ -35,14 +35,15 @@ permissions: jobs: test-sdist: name: Test sdist builds - if: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).jobs.sdist_tests }} + if: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).jobs.sdist_package_roots[fromJSON(inputs.bindings-package).package_root] }} env: BINDINGS_ROOT: ${{ fromJSON(inputs.bindings-package).package_root }} CUDA_PYTHON_BUILD_MAJOR: ${{ fromJSON(inputs.bindings-package).cuda_major }} BUILD_PATHFINDER: ${{ fromJSON(inputs.bindings-package).release_status == 'current' && (inputs.workplan == '' || fromJSON(inputs.workplan).modules.pathfinder.needs_build) }} - BUILD_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_build }} - BUILD_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_build }} - BUILD_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.needs_build }} + BUILD_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.package_roots[fromJSON(inputs.bindings-package).package_root].needs_build }} + BUILD_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.cuda_majors[fromJSON(inputs.bindings-package).cuda_variant].needs_build }} + BUILD_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.package_roots[fromJSON(inputs.bindings-package).package_root].needs_build }} + PATHFINDER_SOURCE: ${{ inputs.workplan == '' && 'artifact' || fromJSON(inputs.workplan).sources.pathfinder }} timeout-minutes: 60 runs-on: windows-2022 steps: @@ -93,7 +94,8 @@ jobs: pip wheel --no-deps --wheel-dir cuda_python/dist cuda_python/dist/*.tar.gz - name: Download cuda.pathfinder wheel - if: ${{ env.BUILD_PATHFINDER != 'true' && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') }} + if: ${{ env.BUILD_PATHFINDER != 'true' && (env.BUILD_BINDINGS == 'true' || env.BUILD_CORE == 'true') && + env.PATHFINDER_SOURCE == 'artifact' }} uses: ./.github/actions/download-artifact-retry with: name: cuda-pathfinder-wheel @@ -102,12 +104,19 @@ jobs: - name: Constrain builds to the local cuda.pathfinder wheel if: ${{ env.BUILD_BINDINGS == 'true' }} run: | - pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl) - test "${#pathfinder_wheels[@]}" -eq 1 - test -f "${pathfinder_wheels[0]}" mkdir -p wheel-constraints - pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" - printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" | tee wheel-constraints/cuda-bindings.txt + if [[ "${PATHFINDER_SOURCE}" == artifact ]]; then + pathfinder_wheels=(cuda_pathfinder/dist/cuda_pathfinder-*.whl) + test "${#pathfinder_wheels[@]}" -eq 1 + test -f "${pathfinder_wheels[0]}" + pathfinder_uri="file:///$(cygpath -am "${pathfinder_wheels[0]}")" + printf 'cuda-pathfinder @ %s\n' "${pathfinder_uri}" > wheel-constraints/cuda-bindings.txt + elif [[ "${PATHFINDER_SOURCE}" == published ]]; then + : > wheel-constraints/cuda-bindings.txt + else + echo "Error: PATHFINDER_SOURCE must be artifact or published" >&2 + exit 1 + fi # Cython packages need CTK. No sccache on Windows (this is a correctness # smoke test, not a production build; see build-wheel.yml which also diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 14f6698cd31..aa86c4dd838 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -14,10 +14,11 @@ on: type: string required: true build-ctk-ver: + description: Build toolkit for the empty-registry legacy snapshot route. type: string - required: true + default: '' bindings-config: - description: Normalized bindings registry; empty retains legacy artifact sources. + description: Normalized registry; empty retains legacy snapshot routing. type: string default: '' matrix_filter: @@ -56,10 +57,15 @@ defaults: jobs: compute-matrix: + if: >- + ${{ inputs.bindings-config == '' || inputs.workplan == '' || + contains(fromJSON(inputs.workplan).jobs.test_cuda_majors.*, true) }} runs-on: ubuntu-latest env: - BUILD_TYPE: ${{ inputs.build-type }} BINDINGS_CONFIG: ${{ inputs.bindings-config }} + BUILD_TYPE: ${{ inputs.build-type }} + WORKPLAN: ${{ inputs.workplan }} + TEST_MODE: ${{ inputs.test-mode }} ARCH: ${{ (inputs.host-platform == 'linux-64' && 'amd64') || (inputs.host-platform == 'linux-aarch64' && 'arm64') }} outputs: @@ -68,6 +74,16 @@ jobs: steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.sha || github.sha }} + + - name: Checkout CI tools (workflow revision) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + path: .ci-control + sparse-checkout: ci/tools + persist-credentials: false - name: Validate Test Type run: | @@ -88,42 +104,34 @@ jobs: # Read base matrix from YAML file for the specific architecture TEST_MATRIX=$(yq -o json ".linux[\"${MATRIX_TYPE}\"] | map(select(.ARCH == \"${ARCH}\"))" ci/test-matrix.yml) - if [[ -n "$BINDINGS_CONFIG" ]]; then - TEST_MATRIX=$(jq -c --argjson config "$BINDINGS_CONFIG" ' - ($config.package_roots | map({key: .toolkit_version, value: .}) | from_entries) as $packages - | map(($packages[.CUDA_VER]) as $package - | . + { - BINDINGS_ROOT: ($package.package_root // "cuda_bindings_13"), - BINDINGS_BUILD_VERSION: ($package.toolkit_version // ""), - BINDINGS_SOURCE: (if $package == null then "floor" else "main" end), - BINDINGS_ARTIFACT_PREFIX: "cuda-bindings", - CORE_CYTHON_SUFFIX: (if $package.release_status == "maintenance" then "-cu\($package.cuda_major)-tests" else "-tests" end), - PYTHON_ARTIFACT_NAME: (if $package == null then "" else "cuda-python-wheel-cuda\($package.toolkit_version)" end) - }) - ' <<< "$TEST_MATRIX") + if [[ -z "$BINDINGS_CONFIG" ]]; then + MATRIX=$(echo "$TEST_MATRIX" | jq -c '${{ inputs.matrix_filter }} | if any(.[]; .DRIVER != "latest" and .DRIVER != "earliest" and .FLAVOR == "wsl") then "Error: custom DRIVER is not supported with FLAVOR=wsl\n" | halt_error(1) else . end | map(. + {RUNNER_DRIVER: (if .DRIVER == "latest" or .DRIVER == "earliest" then .DRIVER else "latest" end)}) | if (. | length) > 0 then {include: .} else "Error: Empty matrix\n" | halt_error(1) end') + OLD_BRANCH=$(yq -r '.backport_branch' ci/versions.yml) + echo "OLD_BRANCH=${OLD_BRANCH}" >> "$GITHUB_OUTPUT" + else + # Keep the caller's free-form jq filter; shared Python code handles + # source selection, test suites, runner routing, and coverage checks. + MATRIX=$(jq -c '${{ inputs.matrix_filter }}' <<< "$TEST_MATRIX" \ + | python3 .ci-control/ci/tools/select_test_matrix.py --platform linux) + + { + echo '### Selected wheel test coverage' + echo '| Suites | Python | CTK | Toolkit source | Bindings source | GPU |' + echo '| --- | --- | --- | --- | --- | --- |' + jq -r '.include[] | "| \(.SUITES) | \(.PY_VER) | \(.CUDA_VER) | \(if .LOCAL_CTK == "1" then "local CTK" else "wheels" end) | \(if .BINDINGS_SOURCE == "local" then .BINDINGS_SOURCE_DIR else "published floor" end) | \(.GPU) |"' <<< "$MATRIX" + } >> "$GITHUB_STEP_SUMMARY" fi - # Apply matrix filter; reject custom DRIVER + FLAVOR=wsl (the - # in-container driver swap doesn't work under WSL); add a - # RUNNER_DRIVER field that maps any custom version back to - # 'latest' (the install script swaps the driver itself, so we - # need to land on the runner that ships with the most recent - # pre-installed driver); wrap in include structure. - MATRIX=$(echo "$TEST_MATRIX" | jq -c '${{ inputs.matrix_filter }} | if any(.[]; .DRIVER != "latest" and .DRIVER != "earliest" and .FLAVOR == "wsl") then "Error: custom DRIVER is not supported with FLAVOR=wsl\n" | halt_error(1) else . end | map(. + {RUNNER_DRIVER: (if .DRIVER == "latest" or .DRIVER == "earliest" then .DRIVER else "latest" end)}) | if (. | length) > 0 then {include: .} else "Error: Empty matrix\n" | halt_error(1) end') - echo "MATRIX=${MATRIX}" | tee --append "${GITHUB_OUTPUT}" - # This job has yq already installed, so let's do it here - OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml) - echo "OLD_BRANCH=${OLD_BRANCH}" >> "$GITHUB_OUTPUT" - test: env: + BINDINGS_CONFIG: ${{ inputs.bindings-config }} TEST_PATHFINDER: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.pathfinder.needs_test }} - TEST_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_test }} - TEST_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_test }} - TEST_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.needs_test }} - name: Python ${{ matrix.PY_VER }}, CUDA ${{ matrix.CUDA_VER }} (${{ (matrix.LOCAL_CTK == '1' && 'local') || 'wheels' }}), GPU ${{ matrix.GPU }}${{ matrix.GPU_COUNT != '1' && format(' (x{0})', matrix.GPU_COUNT) || '' }}${{ matrix.FLAVOR && format(', {0}', matrix.FLAVOR) || '' }}${{ matrix.ENV.TORCH_VER && format(', {0}+{1}', matrix.ENV.TORCH_VER, matrix.ENV.TORCH_CUDA) || '' }}${{ matrix.ENV.MODE == 'nightly-numba-cuda' && ', latest' || '' }} + TEST_BINDINGS: ${{ inputs.workplan == '' || (inputs.bindings-config == '' && fromJSON(inputs.workplan).modules.bindings.needs_test) || (inputs.bindings-config != '' && matrix.BINDINGS_SOURCE == 'local' && fromJSON(inputs.workplan).modules.bindings.package_roots[matrix.BINDINGS_SOURCE_DIR].needs_test) }} + TEST_CORE: ${{ inputs.workplan == '' || (inputs.bindings-config == '' && fromJSON(inputs.workplan).modules.core.needs_test) || (inputs.bindings-config != '' && fromJSON(inputs.workplan).modules.core.cuda_majors[matrix.CUDA_VARIANT].needs_test) }} + TEST_PYTHON: ${{ inputs.workplan == '' || (inputs.bindings-config == '' && fromJSON(inputs.workplan).modules.python.needs_test) || (inputs.bindings-config != '' && matrix.BINDINGS_SOURCE == 'local' && fromJSON(inputs.workplan).modules.python.package_roots[matrix.BINDINGS_SOURCE_DIR].needs_test) }} + name: ${{ matrix.SUITES || inputs.test-mode }} / py${{ matrix.PY_VER }}, CTK${{ matrix.CUDA_VER }} (${{ (matrix.LOCAL_CTK == '1' && 'local CTK') || 'wheels' }}; bindings ${{ matrix.BINDINGS_SOURCE == 'local' && 'PR' || 'floor' }}), GPU ${{ matrix.GPU }}${{ matrix.GPU_COUNT != '1' && format(' (x{0})', matrix.GPU_COUNT) || '' }}${{ matrix.FLAVOR && format(', {0}', matrix.FLAVOR) || '' }}${{ matrix.ENV.TORCH_VER && format(', {0}+{1}', matrix.ENV.TORCH_VER, matrix.ENV.TORCH_CUDA) || '' }}${{ matrix.ENV.MODE == 'nightly-numba-cuda' && ', latest' || '' }} timeout-minutes: 60 needs: compute-matrix strategy: @@ -131,7 +139,8 @@ jobs: matrix: ${{ fromJSON(needs.compute-matrix.outputs.MATRIX) }} runs-on: "${{ matrix.FLAVOR || 'linux' }}-${{ matrix.ARCH }}-gpu-${{ matrix.GPU }}-${{ matrix.RUNNER_DRIVER }}-${{ matrix.GPU_COUNT }}" # The build stage could fail but we want the CI to keep moving. - if: ${{ github.repository_owner == 'nvidia' && !cancelled() }} + if: ${{ github.repository_owner == 'nvidia' && !cancelled() && + (inputs.bindings-config == '' || inputs.workplan == '' || contains(fromJSON(inputs.workplan).jobs.test_cuda_majors.*, true)) }} # Our self-hosted runners require a container # TODO: use a different (nvidia?) container container: @@ -151,29 +160,27 @@ jobs: # reuse wheels built from an older commit of main). ref: ${{ inputs.sha || github.sha }} + - name: Checkout CI tools (workflow revision) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + path: .ci-control + sparse-checkout: ci/tools + persist-credentials: false + - name: Setup proxy cache uses: nv-gha-runners/setup-proxy-cache@main continue-on-error: true - name: Install dependencies - uses: ./.github/actions/install_unix_deps + uses: $/.github/actions/install_unix_deps continue-on-error: false with: # for artifact fetching, graphics libs, g++ required for cffi in # example; util-linux for `nsenter` (custom-DRIVER rows re-exec # install_gpu_driver.sh onto the host through nsenter) - # yq: ci/tools/env-vars reads ci/versions.yml with it - dependencies: "jq wget libgl1 libegl1 g++ util-linux yq" - dependent_exes: "jq wget yq" - - - name: Checkout driver install scripts (workflow revision) - if: ${{ matrix.DRIVER != 'latest' && matrix.DRIVER != 'earliest' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - path: .driver-tools - sparse-checkout: ci/tools - persist-credentials: false + dependencies: "jq wget libgl1 libegl1 g++ util-linux" + dependent_exes: "jq wget" - name: Install GPU driver if: ${{ matrix.DRIVER != 'latest' && matrix.DRIVER != 'earliest' }} @@ -181,26 +188,25 @@ jobs: DRIVER: ${{ matrix.DRIVER }} DRIVER_SHA256: ${{ matrix.DRIVER_SHA256 }} GPU_TYPE: ${{ matrix.GPU }} - run: ./.driver-tools/ci/tools/install_gpu_driver.sh + run: ./.ci-control/ci/tools/install_gpu_driver.sh - name: Ensure GPU is working run: nvidia-smi - name: Set environment variables env: - BUILD_CUDA_VER: ${{ matrix.BINDINGS_BUILD_VERSION || inputs.build-ctk-ver }} - CUDA_BINDINGS_ROOT: ${{ matrix.BINDINGS_ROOT || 'cuda_bindings_13' }} - BINDINGS_SOURCE: ${{ matrix.BINDINGS_SOURCE || '' }} - BINDINGS_ARTIFACT_PREFIX: ${{ matrix.BINDINGS_ARTIFACT_PREFIX || 'cuda-bindings' }} - CORE_CYTHON_SUFFIX: ${{ matrix.CORE_CYTHON_SUFFIX || '-tests' }} - CUDA_PYTHON_ARTIFACT_NAME: ${{ matrix.PYTHON_ARTIFACT_NAME || 'cuda-python-wheel' }} + BUILD_CUDA_VER: ${{ inputs.build-ctk-ver }} + BINDINGS_SOURCE: ${{ matrix.BINDINGS_SOURCE }} + BINDINGS_SOURCE_DIR: ${{ matrix.BINDINGS_SOURCE_DIR }} + DEFAULT_BINDINGS_SOURCE_DIR: ${{ matrix.DEFAULT_BINDINGS_SOURCE_DIR }} CUDA_VER: ${{ matrix.CUDA_VER }} HOST_PLATFORM: ${{ inputs.host-platform }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} + PATHFINDER_SOURCE: ${{ matrix.PATHFINDER_SOURCE || 'artifact' }} PY_VER: ${{ matrix.PY_VER }} SHA: ${{ inputs.sha || github.sha }} SKIP_BINDINGS_TEST_OVERRIDE: ${{ env.TEST_BINDINGS != 'true' && '1' || '0' }} - run: ./ci/tools/env-vars test + run: ./.ci-control/ci/tools/env-vars test - name: Apply extra matrix environment variables if: ${{ matrix.ENV }} @@ -209,7 +215,8 @@ jobs: run: echo "$MATRIX_ENV" | jq -r 'to_entries[] | "\(.key)=\(.value)"' >> "$GITHUB_ENV" - name: Download cuda-pathfinder build artifacts - if: ${{ env.TEST_PATHFINDER == 'true' || env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true' }} + if: ${{ (env.TEST_PATHFINDER == 'true' || env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') && + env.PATHFINDER_SOURCE == 'artifact' }} uses: $/.github/actions/download-artifact-retry with: name: cuda-pathfinder-wheel @@ -218,17 +225,17 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Download cuda-python build artifacts - if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE == 'main' }} + if: ${{ env.TEST_PYTHON == 'true' && (env.BINDINGS_SOURCE == 'local' || env.BINDINGS_SOURCE == 'main') }} uses: $/.github/actions/download-artifact-retry with: - name: ${{ env.CUDA_PYTHON_ARTIFACT_NAME || 'cuda-python-wheel' }} + name: ${{ env.CUDA_PYTHON_ARTIFACT_NAME }} path: . run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Download cuda.bindings build artifacts if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') && - env.BINDINGS_SOURCE == 'main' }} + (env.BINDINGS_SOURCE == 'local' || env.BINDINGS_SOURCE == 'main') }} uses: $/.github/actions/download-artifact-retry with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} @@ -242,6 +249,7 @@ jobs: env.BINDINGS_SOURCE == 'backport' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OLD_BRANCH: ${{ needs.compute-matrix.outputs.OLD_BRANCH }} run: | # See https://github.com/cli/cli/blob/trunk/docs/install_linux.md#debian-ubuntu-linux-raspberry-pi-os-apt. # gh is needed for lookup-run-id, which calls gh api. @@ -253,7 +261,6 @@ jobs: && apt update \ && apt install gh -y - OLD_BRANCH=${{ needs.compute-matrix.outputs.OLD_BRANCH }} OLD_ARTIFACT_PATTERN="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}-*[0-9a-f]" LOOKUP_ARGS=( --branch "${OLD_BRANCH}" @@ -262,7 +269,7 @@ jobs: if ${{ env.TEST_PYTHON == 'true' }}; then LOOKUP_ARGS+=(--artifact cuda-python-wheel) fi - LATEST_PRIOR_RUN_ID=$(./ci/tools/lookup-run-id \ + LATEST_PRIOR_RUN_ID=$(./.ci-control/ci/tools/lookup-run-id \ "${LOOKUP_ARGS[@]}" NVIDIA/cuda-python "CI") echo "run-id=${LATEST_PRIOR_RUN_ID}" >> "${GITHUB_OUTPUT}" echo "artifact-pattern=${OLD_ARTIFACT_PATTERN}" >> "${GITHUB_OUTPUT}" @@ -299,14 +306,14 @@ jobs: ls -al "${CUDA_BINDINGS_ARTIFACTS_DIR}" - name: Display structure of downloaded cuda-python artifacts - if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'floor' }} + if: ${{ env.TEST_PYTHON == 'true' && (env.BINDINGS_SOURCE == 'local' || env.BINDINGS_SOURCE == 'main') }} run: | pwd ls -lah cuda_python*.whl cuda_pathfinder/ - name: Display structure of downloaded cuda.bindings artifacts if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') && - env.BINDINGS_SOURCE != 'floor' }} + (env.BINDINGS_SOURCE == 'local' || env.BINDINGS_SOURCE == 'main') }} run: | pwd ls -lahR $CUDA_BINDINGS_ARTIFACTS_DIR @@ -345,7 +352,7 @@ jobs: if: ${{ env.TEST_CORE == 'true' && env.SKIP_CYTHON_TEST == '0' }} uses: $/.github/actions/download-artifact-retry with: - name: ${{ env.CUDA_CORE_CYTHON_TEST_ARTIFACT_NAME || format('{0}-tests', env.CUDA_CORE_ARTIFACT_NAME) }} + name: ${{ env.CUDA_CORE_CYTHON_TEST_ARTIFACT_NAME }} path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }} run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} @@ -380,7 +387,7 @@ jobs: - name: Set up mini CTK if: ${{ matrix.LOCAL_CTK == '1' }} - uses: ./.github/actions/fetch_ctk + uses: $/.github/actions/fetch_ctk continue-on-error: false with: host-platform: ${{ inputs.host-platform }} @@ -388,7 +395,7 @@ jobs: - name: Set up latest cuda_sanitizer_api if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true') && env.SETUP_SANITIZER == '1' }} - uses: ./.github/actions/fetch_ctk + uses: $/.github/actions/fetch_ctk continue-on-error: false with: host-platform: ${{ inputs.host-platform }} @@ -439,22 +446,25 @@ jobs: run: run-tests core - name: Ensure cuda-python installable - if: ${{ inputs.test-mode == 'standard' && env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE == 'main' }} + if: ${{ inputs.test-mode == 'standard' && env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'floor' }} + env: + LOCAL_CTK: ${{ matrix.LOCAL_CTK }} + shell: bash --noprofile --norc -xeuo pipefail {0} run: | - # Package suites install their own dependencies. A metapackage-only - # run has no preceding suite, so install the exact local internal - # wheels in one transaction while resolving released dependencies - # such as cuda-core from the package index. - if ${{ env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' }}; then + # CUDA 13's metapackage also requires Core. A bindings-only release + # must resolve published Core while preserving the selected wheels. + if [[ "$TEST_CORE" == true || ("$TEST_BINDINGS" == true && "$TEST_CUDA_MAJOR" == 12) ]]; then dependency_args=(--no-deps) else - dependency_args=( - ./cuda_pathfinder/cuda_pathfinder-*.whl - "${CUDA_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-*.whl - ) + dependency_args=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-*.whl) + if [[ "$PATHFINDER_SOURCE" == artifact ]]; then + dependency_args+=(./cuda_pathfinder/cuda_pathfinder-*.whl) + else + dependency_args+=("$(python -c 'from importlib.metadata import version; print("cuda-pathfinder==" + version("cuda-pathfinder"))')") + fi fi python_requirements=(cuda_python*.whl) - if [[ "${{ matrix.LOCAL_CTK }}" != 1 ]]; then + if [[ "$LOCAL_CTK" != 1 ]]; then python_requirements=("${python_requirements[@]/%/[all]}") fi pip install --only-binary=:all: "${dependency_args[@]}" "${python_requirements[@]}" @@ -510,7 +520,7 @@ jobs: if: ${{ inputs.test-mode == 'nightly-pytorch' }} run: | pushd cuda_core - pytest -rsxXfE -v --exclude-warning-annotations --junitxml "${GITHUB_WORKSPACE}/test-results/junit-pytorch-interop.xml" tests/test_utils.py tests/example_tests/ | python "${GITHUB_WORKSPACE}/ci/tools/fold_pytest_sections.py" + pytest -rsxXfE -v --exclude-warning-annotations --junitxml "${GITHUB_WORKSPACE}/test-results/junit-pytorch-interop.xml" tests/test_utils.py tests/example_tests/ | python "${GITHUB_WORKSPACE}/.ci-control/ci/tools/fold_pytest_sections.py" popd - name: Checkout numba-cuda tests at matching tag @@ -572,7 +582,7 @@ jobs: --ignore=tests/benchmarks \ --ignore=tests/doc_examples \ --deselect 'tests/numba_cuda_tests/cudadrv/test_cuda_devicerecord.py::TestCudaDeviceRecordWithRecord::test_device_record_copy' \ - tests/ | python "${GITHUB_WORKSPACE}/ci/tools/fold_pytest_sections.py" + tests/ | python "${GITHUB_WORKSPACE}/.ci-control/ci/tools/fold_pytest_sections.py" popd - name: Checkout released cuda-core tests at matching tag @@ -617,7 +627,7 @@ jobs: pytest -rsxXfE -v --randomly-dont-reorganize \ --junitxml "${GITHUB_WORKSPACE}/test-results/junit-cuda-core-released.xml" \ "${DESELECTS[@]}" \ - tests/ | python "${GITHUB_WORKSPACE}/ci/tools/fold_pytest_sections.py" + tests/ | python "${GITHUB_WORKSPACE}/.ci-control/ci/tools/fold_pytest_sections.py" popd - name: Upload test results diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index 541900b3006..c66f4408a36 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -14,10 +14,11 @@ on: type: string required: true build-ctk-ver: + description: Build toolkit for the empty-registry legacy snapshot route. type: string - required: true + default: '' bindings-config: - description: Normalized bindings registry; empty retains legacy artifact sources. + description: Normalized registry; empty retains legacy snapshot routing. type: string default: '' matrix_filter: @@ -52,19 +53,35 @@ on: jobs: compute-matrix: + if: >- + ${{ inputs.bindings-config == '' || inputs.workplan == '' || + contains(fromJSON(inputs.workplan).jobs.test_cuda_majors.*, true) }} runs-on: ubuntu-latest defaults: run: shell: bash --noprofile --norc -xeuo pipefail {0} env: - BUILD_TYPE: ${{ inputs.build-type }} BINDINGS_CONFIG: ${{ inputs.bindings-config }} + BUILD_TYPE: ${{ inputs.build-type }} + WORKPLAN: ${{ inputs.workplan }} + TEST_MODE: ${{ inputs.test-mode }} ARCH: ${{ (inputs.host-platform == 'win-64' && 'amd64') }} outputs: MATRIX: ${{ steps.compute-matrix.outputs.MATRIX }} + OLD_BRANCH: ${{ steps.compute-matrix.outputs.OLD_BRANCH }} steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.sha || github.sha }} + + - name: Checkout CI tools (workflow revision) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + path: .ci-control + sparse-checkout: ci/tools + persist-credentials: false - name: Validate Test Type run: | @@ -84,43 +101,42 @@ jobs: # Read base matrix from YAML file for the specific architecture TEST_MATRIX=$(yq -o json ".windows[\"${MATRIX_TYPE}\"] | map(select(.ARCH == \"${ARCH}\"))" ci/test-matrix.yml) - if [[ -n "$BINDINGS_CONFIG" ]]; then - TEST_MATRIX=$(jq -c --argjson config "$BINDINGS_CONFIG" ' - ($config.package_roots | map({key: .toolkit_version, value: .}) | from_entries) as $packages - | map(($packages[.CUDA_VER]) as $package - | . + { - BINDINGS_ROOT: ($package.package_root // "cuda_bindings_13"), - BINDINGS_BUILD_VERSION: ($package.toolkit_version // ""), - BINDINGS_SOURCE: (if $package == null then "floor" else "main" end), - BINDINGS_ARTIFACT_PREFIX: "cuda-bindings", - CORE_CYTHON_SUFFIX: (if $package.release_status == "maintenance" then "-cu\($package.cuda_major)-tests" else "-tests" end), - PYTHON_ARTIFACT_NAME: (if $package == null then "" else "cuda-python-wheel-cuda\($package.toolkit_version)" end) - }) - ' <<< "$TEST_MATRIX") + if [[ -z "$BINDINGS_CONFIG" ]]; then + MATRIX=$(echo "$TEST_MATRIX" | jq -c '${{ inputs.matrix_filter }} | map(. + {RUNNER_DRIVER: (if .DRIVER == "latest" or .DRIVER == "earliest" then .DRIVER else "latest" end)}) | if (. | length) > 0 then {include: .} else "Error: Empty matrix\n" | halt_error(1) end') + OLD_BRANCH=$(yq -r '.backport_branch' ci/versions.yml) + echo "OLD_BRANCH=${OLD_BRANCH}" >> "$GITHUB_OUTPUT" + else + # Keep the caller's free-form jq filter; shared Python code handles + # source selection, test suites, runner routing, and coverage checks. + MATRIX=$(jq -c '${{ inputs.matrix_filter }}' <<< "$TEST_MATRIX" \ + | python3 .ci-control/ci/tools/select_test_matrix.py --platform windows) + + { + echo '### Selected wheel test coverage' + echo '| Suites | Python | CTK | Toolkit source | Bindings source | GPU |' + echo '| --- | --- | --- | --- | --- | --- |' + jq -r '.include[] | "| \(.SUITES) | \(.PY_VER) | \(.CUDA_VER) | \(if .LOCAL_CTK == "1" then "local CTK" else "wheels" end) | \(if .BINDINGS_SOURCE == "local" then .BINDINGS_SOURCE_DIR else "published floor" end) | \(.GPU) |"' <<< "$MATRIX" + } >> "$GITHUB_STEP_SUMMARY" fi - # Apply matrix filter; add a RUNNER_DRIVER field that maps any - # custom DRIVER version back to 'latest' (install_gpu_driver.ps1 - # swaps the driver itself, so the runner must be the one that - # ships the most recent pre-installed driver); wrap in include. - MATRIX=$(echo "$TEST_MATRIX" | jq -c '${{ inputs.matrix_filter }} | map(. + {RUNNER_DRIVER: (if .DRIVER == "latest" or .DRIVER == "earliest" then .DRIVER else "latest" end)}) | if (. | length) > 0 then {include: .} else "Error: Empty matrix\n" | halt_error(1) end') - echo "MATRIX=${MATRIX}" | tee --append "${GITHUB_OUTPUT}" test: env: + BINDINGS_CONFIG: ${{ inputs.bindings-config }} TEST_PATHFINDER: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.pathfinder.needs_test }} - TEST_BINDINGS: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.bindings.needs_test }} - TEST_CORE: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.core.needs_test }} - TEST_PYTHON: ${{ inputs.workplan == '' || fromJSON(inputs.workplan).modules.python.needs_test }} - name: Python ${{ matrix.PY_VER }}, CUDA ${{ matrix.CUDA_VER }} (${{ (matrix.LOCAL_CTK == '1' && 'local') || 'wheels' }}), GPU ${{ matrix.GPU }}${{ matrix.GPU_COUNT != '1' && format(' (x{0})', matrix.GPU_COUNT) || '' }} (${{ matrix.DRIVER_MODE }})${{ matrix.ENV.TORCH_VER && format(', {0}+{1}', matrix.ENV.TORCH_VER, matrix.ENV.TORCH_CUDA) || '' }}${{ matrix.ENV.MODE == 'nightly-numba-cuda' && ', latest' || '' }} + TEST_BINDINGS: ${{ inputs.workplan == '' || (inputs.bindings-config == '' && fromJSON(inputs.workplan).modules.bindings.needs_test) || (inputs.bindings-config != '' && matrix.BINDINGS_SOURCE == 'local' && fromJSON(inputs.workplan).modules.bindings.package_roots[matrix.BINDINGS_SOURCE_DIR].needs_test) }} + TEST_CORE: ${{ inputs.workplan == '' || (inputs.bindings-config == '' && fromJSON(inputs.workplan).modules.core.needs_test) || (inputs.bindings-config != '' && fromJSON(inputs.workplan).modules.core.cuda_majors[matrix.CUDA_VARIANT].needs_test) }} + TEST_PYTHON: ${{ inputs.workplan == '' || (inputs.bindings-config == '' && fromJSON(inputs.workplan).modules.python.needs_test) || (inputs.bindings-config != '' && matrix.BINDINGS_SOURCE == 'local' && fromJSON(inputs.workplan).modules.python.package_roots[matrix.BINDINGS_SOURCE_DIR].needs_test) }} + name: ${{ matrix.SUITES || inputs.test-mode }} / py${{ matrix.PY_VER }}, CTK${{ matrix.CUDA_VER }} (${{ (matrix.LOCAL_CTK == '1' && 'local CTK') || 'wheels' }}; bindings ${{ matrix.BINDINGS_SOURCE == 'local' && 'PR' || 'floor' }}), GPU ${{ matrix.GPU }}${{ matrix.GPU_COUNT != '1' && format(' (x{0})', matrix.GPU_COUNT) || '' }} (${{ matrix.DRIVER_MODE }})${{ matrix.ENV.TORCH_VER && format(', {0}+{1}', matrix.ENV.TORCH_VER, matrix.ENV.TORCH_CUDA) || '' }}${{ matrix.ENV.MODE == 'nightly-numba-cuda' && ', latest' || '' }} timeout-minutes: 60 # The build stage could fail but we want the CI to keep moving. needs: compute-matrix strategy: fail-fast: false matrix: ${{ fromJSON(needs.compute-matrix.outputs.MATRIX) }} - if: ${{ github.repository_owner == 'nvidia' && !cancelled() }} + if: ${{ github.repository_owner == 'nvidia' && !cancelled() && + (inputs.bindings-config == '' || inputs.workplan == '' || contains(fromJSON(inputs.workplan).jobs.test_cuda_majors.*, true)) }} runs-on: "windows-${{ matrix.ARCH }}-gpu-${{ matrix.GPU }}-${{ matrix.RUNNER_DRIVER }}-${{ matrix.GPU_COUNT }}" steps: - name: Enable Git symlinks (Windows, must precede checkout) @@ -133,21 +149,20 @@ jobs: # reuse wheels built from an older commit of main). ref: ${{ inputs.sha || github.sha }} - - name: Setup proxy cache - uses: nv-gha-runners/setup-proxy-cache@main - continue-on-error: true - with: - enable-apt: true - - - name: Checkout driver install scripts (workflow revision) - if: ${{ matrix.DRIVER != 'latest' && matrix.DRIVER != 'earliest' }} + - name: Checkout CI tools (workflow revision) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.sha }} - path: .driver-tools + path: .ci-control sparse-checkout: ci/tools persist-credentials: false + - name: Setup proxy cache + uses: nv-gha-runners/setup-proxy-cache@main + continue-on-error: true + with: + enable-apt: true + - name: Install GPU driver if: ${{ matrix.DRIVER != 'latest' && matrix.DRIVER != 'earliest' }} env: @@ -155,13 +170,13 @@ jobs: DRIVER_SHA256: ${{ matrix.DRIVER_SHA256 }} GPU_TYPE: ${{ matrix.GPU }} run: | - .driver-tools/ci/tools/install_gpu_driver.ps1 + .ci-control/ci/tools/install_gpu_driver.ps1 - name: Configure driver mode env: DRIVER_MODE: ${{ matrix.DRIVER_MODE }} run: | - ci/tools/configure_driver_mode.ps1 + .ci-control/ci/tools/configure_driver_mode.ps1 - name: Ensure GPU is working run: | @@ -177,20 +192,19 @@ jobs: - name: Set environment variables env: - BUILD_CUDA_VER: ${{ matrix.BINDINGS_BUILD_VERSION || inputs.build-ctk-ver }} - CUDA_BINDINGS_ROOT: ${{ matrix.BINDINGS_ROOT || 'cuda_bindings_13' }} - BINDINGS_SOURCE: ${{ matrix.BINDINGS_SOURCE || '' }} - BINDINGS_ARTIFACT_PREFIX: ${{ matrix.BINDINGS_ARTIFACT_PREFIX || 'cuda-bindings' }} - CORE_CYTHON_SUFFIX: ${{ matrix.CORE_CYTHON_SUFFIX || '-tests' }} - CUDA_PYTHON_ARTIFACT_NAME: ${{ matrix.PYTHON_ARTIFACT_NAME || 'cuda-python-wheel' }} + BUILD_CUDA_VER: ${{ inputs.build-ctk-ver }} + BINDINGS_SOURCE: ${{ matrix.BINDINGS_SOURCE }} + BINDINGS_SOURCE_DIR: ${{ matrix.BINDINGS_SOURCE_DIR }} + DEFAULT_BINDINGS_SOURCE_DIR: ${{ matrix.DEFAULT_BINDINGS_SOURCE_DIR }} CUDA_VER: ${{ matrix.CUDA_VER }} HOST_PLATFORM: ${{ inputs.host-platform }} LOCAL_CTK: ${{ matrix.LOCAL_CTK }} + PATHFINDER_SOURCE: ${{ matrix.PATHFINDER_SOURCE || 'artifact' }} PY_VER: ${{ matrix.PY_VER }} SHA: ${{ inputs.sha || github.sha }} SKIP_BINDINGS_TEST_OVERRIDE: ${{ env.TEST_BINDINGS != 'true' && '1' || '0' }} shell: bash --noprofile --norc -xeuo pipefail {0} - run: ./ci/tools/env-vars test + run: ./.ci-control/ci/tools/env-vars test - name: Apply extra matrix environment variables if: ${{ matrix.ENV }} @@ -200,7 +214,8 @@ jobs: run: echo "$MATRIX_ENV" | jq -r 'to_entries[] | "\(.key)=\(.value)"' >> "$GITHUB_ENV" - name: Download cuda-pathfinder build artifacts - if: ${{ env.TEST_PATHFINDER == 'true' || env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true' }} + if: ${{ (env.TEST_PATHFINDER == 'true' || env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') && + env.PATHFINDER_SOURCE == 'artifact' }} uses: $/.github/actions/download-artifact-retry with: name: cuda-pathfinder-wheel @@ -209,17 +224,17 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Download cuda-python build artifacts - if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE == 'main' }} + if: ${{ env.TEST_PYTHON == 'true' && (env.BINDINGS_SOURCE == 'local' || env.BINDINGS_SOURCE == 'main') }} uses: $/.github/actions/download-artifact-retry with: - name: ${{ env.CUDA_PYTHON_ARTIFACT_NAME || 'cuda-python-wheel' }} + name: ${{ env.CUDA_PYTHON_ARTIFACT_NAME }} path: . run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Download cuda.bindings build artifacts if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') && - env.BINDINGS_SOURCE == 'main' }} + (env.BINDINGS_SOURCE == 'local' || env.BINDINGS_SOURCE == 'main') }} uses: $/.github/actions/download-artifact-retry with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} @@ -233,9 +248,9 @@ jobs: env.BINDINGS_SOURCE == 'backport' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OLD_BRANCH: ${{ needs.compute-matrix.outputs.OLD_BRANCH }} shell: bash --noprofile --norc -xeuo pipefail {0} run: | - OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml) OLD_ARTIFACT_PATTERN="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}-*[0-9a-f]" LOOKUP_ARGS=( --branch "${OLD_BRANCH}" @@ -244,7 +259,7 @@ jobs: if ${{ env.TEST_PYTHON == 'true' }}; then LOOKUP_ARGS+=(--artifact cuda-python-wheel) fi - LATEST_PRIOR_RUN_ID=$(./ci/tools/lookup-run-id \ + LATEST_PRIOR_RUN_ID=$(./.ci-control/ci/tools/lookup-run-id \ "${LOOKUP_ARGS[@]}" NVIDIA/cuda-python "CI") echo "run-id=${LATEST_PRIOR_RUN_ID}" >> "${GITHUB_OUTPUT}" echo "artifact-pattern=${OLD_ARTIFACT_PATTERN}" >> "${GITHUB_OUTPUT}" @@ -282,14 +297,14 @@ jobs: ls -al "${CUDA_BINDINGS_ARTIFACTS_DIR}" - name: Display structure of downloaded cuda-python artifacts - if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'floor' }} + if: ${{ env.TEST_PYTHON == 'true' && (env.BINDINGS_SOURCE == 'local' || env.BINDINGS_SOURCE == 'main') }} run: | Get-Location Get-ChildItem cuda_python*.whl | Select-Object Mode, LastWriteTime, Length, FullName - name: Display structure of downloaded cuda.bindings artifacts if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') && - env.BINDINGS_SOURCE != 'floor' }} + (env.BINDINGS_SOURCE == 'local' || env.BINDINGS_SOURCE == 'main') }} run: | Get-Location Get-ChildItem -Recurse -Force $env:CUDA_BINDINGS_ARTIFACTS_DIR | Select-Object Mode, LastWriteTime, Length, FullName @@ -328,7 +343,7 @@ jobs: if: ${{ env.TEST_CORE == 'true' && env.SKIP_CYTHON_TEST == '0' }} uses: $/.github/actions/download-artifact-retry with: - name: ${{ env.CUDA_CORE_CYTHON_TEST_ARTIFACT_NAME || format('{0}-tests', env.CUDA_CORE_ARTIFACT_NAME) }} + name: ${{ env.CUDA_CORE_CYTHON_TEST_ARTIFACT_NAME }} path: ${{ env.CUDA_CORE_CYTHON_TESTS_DIR }} run-id: ${{ inputs.run-id || github.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} @@ -372,7 +387,7 @@ jobs: - name: Set up mini CTK if: ${{ matrix.LOCAL_CTK == '1' }} - uses: ./.github/actions/fetch_ctk + uses: $/.github/actions/fetch_ctk continue-on-error: false with: host-platform: ${{ inputs.host-platform }} @@ -409,25 +424,28 @@ jobs: run: run-tests core - name: Ensure cuda-python installable - if: ${{ inputs.test-mode == 'standard' && env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE == 'main' }} + if: ${{ inputs.test-mode == 'standard' && env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'floor' }} + env: + LOCAL_CTK: ${{ matrix.LOCAL_CTK }} + shell: bash --noprofile --norc -xeuo pipefail {0} run: | - # Package suites install their own dependencies. A metapackage-only - # run has no preceding suite, so install the exact local internal - # wheels in one transaction while resolving released dependencies - # such as cuda-core from the package index. - if ('${{ env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' }}' -eq 'true') { - $dependencyArgs = @('--no-deps') - } else { - $dependencyArgs = @( - (Get-Item ./cuda_pathfinder/cuda_pathfinder-*.whl).FullName - (Get-Item "$env:CUDA_BINDINGS_ARTIFACTS_DIR/cuda_bindings-*.whl").FullName - ) - } - $pythonRequirements = @((Get-Item ./cuda_python*.whl).FullName) - if ('${{ matrix.LOCAL_CTK }}' -ne '1') { - $pythonRequirements = @($pythonRequirements | ForEach-Object { "$($_)[all]" }) - } - pip install --only-binary=:all: @dependencyArgs @pythonRequirements + # CUDA 13's metapackage also requires Core. A bindings-only release + # must resolve published Core while preserving the selected wheels. + if [[ "$TEST_CORE" == true || ("$TEST_BINDINGS" == true && "$TEST_CUDA_MAJOR" == 12) ]]; then + dependency_args=(--no-deps) + else + dependency_args=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/cuda_bindings-*.whl) + if [[ "$PATHFINDER_SOURCE" == artifact ]]; then + dependency_args+=(./cuda_pathfinder/cuda_pathfinder-*.whl) + else + dependency_args+=("$(python -c 'from importlib.metadata import version; print("cuda-pathfinder==" + version("cuda-pathfinder"))')") + fi + fi + python_requirements=(cuda_python*.whl) + if [[ "$LOCAL_CTK" != 1 ]]; then + python_requirements=("${python_requirements[@]/%/[all]}") + fi + pip install --only-binary=:all: "${dependency_args[@]}" "${python_requirements[@]}" - name: Install cuda.pathfinder extra wheels for testing if: ${{ inputs.test-mode == 'standard' && env.TEST_PATHFINDER == 'true' }} @@ -493,7 +511,7 @@ jobs: shell: bash --noprofile --norc -xeuo pipefail {0} run: | pushd cuda_core - pytest -rsxXfE -v --exclude-warning-annotations --junitxml "${GITHUB_WORKSPACE}/test-results/junit-pytorch-interop.xml" tests/test_utils.py tests/example_tests/ | python "${GITHUB_WORKSPACE}/ci/tools/fold_pytest_sections.py" + pytest -rsxXfE -v --exclude-warning-annotations --junitxml "${GITHUB_WORKSPACE}/test-results/junit-pytorch-interop.xml" tests/test_utils.py tests/example_tests/ | python "${GITHUB_WORKSPACE}/.ci-control/ci/tools/fold_pytest_sections.py" popd - name: Checkout numba-cuda tests at matching tag @@ -545,7 +563,7 @@ jobs: --ignore=tests/benchmarks \ --ignore=tests/doc_examples \ --deselect 'tests/numba_cuda_tests/cudadrv/test_cuda_devicerecord.py::TestCudaDeviceRecordWithRecord::test_device_record_copy' \ - tests/ | python "${GITHUB_WORKSPACE}/ci/tools/fold_pytest_sections.py" + tests/ | python "${GITHUB_WORKSPACE}/.ci-control/ci/tools/fold_pytest_sections.py" popd - name: Checkout released cuda-core tests at matching tag @@ -592,7 +610,7 @@ jobs: pytest -rsxXfE -v --randomly-dont-reorganize \ --junitxml "${GITHUB_WORKSPACE}/test-results/junit-cuda-core-released.xml" \ "${DESELECTS[@]}" \ - tests/ | python "${GITHUB_WORKSPACE}/ci/tools/fold_pytest_sections.py" + tests/ | python "${GITHUB_WORKSPACE}/.ci-control/ci/tools/fold_pytest_sections.py" popd - name: Upload test results diff --git a/ci/tools/bindings_config.py b/ci/tools/bindings_config.py index 3e81800cb8d..ecf08a2b995 100644 --- a/ci/tools/bindings_config.py +++ b/ci/tools/bindings_config.py @@ -82,6 +82,15 @@ def get_package(self, package_root: str) -> BindingsPackage: def package_for_release_status(self, release_status: str) -> BindingsPackage: return next(package for package in self.package_roots if package.release_status == release_status) + def to_dict(self) -> dict[str, object]: + return { + "python": {"supported": list(self.python_supported)}, + "package_roots": [package.to_dict() for package in self.package_roots], + } + + def to_json(self) -> str: + return json.dumps(self.to_dict(), separators=(",", ":"), sort_keys=True) + def match_tag(self, tag: str) -> BindingsPackage | None: return next((package for package in self.package_roots if package.matches_tag(tag)), None) @@ -208,6 +217,42 @@ def write_github_env(data: Mapping[str, object], path: Path) -> None: stream.write(f"RESOLVED_BINDINGS_PACKAGE={json.dumps(data, separators=(',', ':'))}\n") +def write_build_github_env( + config: BindingsConfig, + path: Path, + *, + repo_root: Path, + python_version: str, + host_platform: str, + sha: str, +) -> None: + """Append both bindings lines' build paths and artifact names for Actions.""" + python_tag = python_version.replace(".", "") + values = {} + for role in ("current", "maintenance"): + package = config.package_for_release_status(role) + prefix = role.upper() + source = repo_root / package.package_root + artifact = f"cuda-bindings-python{python_tag}-cuda{package.toolkit_version}-{host_platform}" + values.update( + { + f"{prefix}_BINDINGS_ROOT": package.package_root, + f"{prefix}_CUDA_VERSION": package.toolkit_version, + f"{prefix}_CUDA_MAJOR": package.cuda_major, + f"{prefix}_CUDA_VARIANT": package.cuda_variant, + f"{prefix}_BINDINGS_ARTIFACT_BASENAME": artifact, + f"{prefix}_BINDINGS_ARTIFACT_NAME": f"{artifact}-{sha}", + # Forward slashes work in native Python and Git Bash on Windows. + f"{prefix}_BINDINGS_ARTIFACTS_DIR": (source / "dist").resolve().as_posix(), + f"{prefix}_BINDINGS_CYTHON_TESTS_DIR": (source / "tests" / "cython").resolve().as_posix(), + f"{prefix}_PYTHON_ARTIFACT_NAME": f"cuda-python-wheel-cuda{package.toolkit_version}", + } + ) + with path.open("a", encoding="utf-8") as stream: + for name, value in values.items(): + stream.write(f"{name}={value}\n") + + def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--config", type=Path, default=DEFAULT_CONFIG) @@ -219,9 +264,25 @@ def main(argv: list[str] | None = None) -> int: commands = parser.add_subparsers(dest="command") write_env = commands.add_parser("write-github-env", help="append package JSON from stdin to GITHUB_ENV") write_env.add_argument("github_env", type=Path, metavar="GITHUB_ENV") + write_build_env = commands.add_parser("write-build-env", help="append both bindings lines' build variables") + write_build_env.add_argument("github_env", type=Path, metavar="GITHUB_ENV") + write_build_env.add_argument("--repo-root", type=Path, default=REPO_ROOT) + write_build_env.add_argument("--python-version", required=True) + write_build_env.add_argument("--host-platform", required=True) + write_build_env.add_argument("--sha", required=True) args = parser.parse_args(argv) try: + if args.command == "write-build-env": + write_build_github_env( + load_config(args.config), + args.github_env, + repo_root=args.repo_root, + python_version=args.python_version, + host_platform=args.host_platform, + sha=args.sha, + ) + return 0 if args.command == "write-github-env": if args.package_roots or args.release_status or args.release_tag or args.release_source_root is not None: parser.error("write-github-env does not accept registry selectors") diff --git a/ci/tools/compute_ci_plan.py b/ci/tools/compute_ci_plan.py index fa90e356c85..a1c3aa82656 100644 --- a/ci/tools/compute_ci_plan.py +++ b/ci/tools/compute_ci_plan.py @@ -12,26 +12,19 @@ import json import subprocess from pathlib import Path, PurePosixPath +from typing import TYPE_CHECKING + +from .bindings_config import load_config + +if TYPE_CHECKING: + from .bindings_config import BindingsConfig REPO_ROOT = Path(__file__).resolve().parents[2] -MODULES = ("pathfinder", "bindings", "core", "python") PLATFORMS = ("linux", "windows") -PACKAGE_MODULES = { - "cuda_pathfinder": "pathfinder", - "cuda_bindings_12": "bindings", - "cuda_bindings_13": "bindings", - "cuda_core": "core", - "cuda_python": "python", -} - -# Source changes have different build and test consumers. In particular, -# cuda-python source needs a same-version bindings wheel, while a core-only -# change can reuse the baseline cuda-python wheel. -SOURCE_IMPACT = { - "pathfinder": (set(MODULES), set(MODULES)), - "bindings": ({"bindings", "core", "python"}, {"bindings", "core", "python"}), - "core": ({"core"}, {"core", "python"}), - "python": ({"bindings", "python"}, {"python"}), +PACKAGE_TARGETS: dict[str, tuple[str, None]] = { + "cuda_pathfinder": ("pathfinder", None), + "cuda_core": ("core", None), + "cuda_python": ("python", None), } IGNORED_BASENAMES = {"AGENTS.md", "CLAUDE.md", "pixi.lock", "pixi.toml"} @@ -63,22 +56,75 @@ "ci/tools/setup-sanitizer": "linux", } +Target = tuple[str, str | None] + def compute_workplan( - paths: list[str], *, + bindings_config: BindingsConfig, + paths: list[str], + linked_paths: set[str], merge_base: str, baseline_run_id: str, - linked_paths: set[str] | None = None, + release_tag: str, ) -> dict[str, object]: """Return the final CI decisions for the supplied changed paths.""" - linked_paths = linked_paths or set() - source_changes: set[str] = set() - test_changes: set[str] = set() + bindings_packages = bindings_config.package_roots + package_roots = tuple(package.package_root for package in bindings_packages) + cuda_variants = tuple(package.cuda_variant for package in bindings_packages) + + bindings_targets = frozenset(("bindings", package_root) for package_root in package_roots) + core_targets = frozenset(("core", variant) for variant in cuda_variants) + python_targets = frozenset(("python", package_root) for package_root in package_roots) + all_targets = frozenset({("pathfinder", None), *bindings_targets, *core_targets, *python_targets}) + + package_targets = { + PurePosixPath(package.package_root): ("bindings", package.package_root) for package in bindings_packages + } + package_targets.update((PurePosixPath(source_dir), target) for source_dir, target in PACKAGE_TARGETS.items()) + + # Source changes have different build/test consumers. Core-only changes can + # reuse baseline cuda-python wheels; cuda-python changes cannot reuse bindings. + # Bindings-only changes reuse the unchanged Core wheel and test it against + # the changed bindings major; Core source or shared changes rebuild both halves. + source_impacts: dict[Target, tuple[frozenset[Target], frozenset[Target], frozenset[str]]] = { + ("pathfinder", None): (all_targets, all_targets, frozenset(package_roots)), + ("core", None): (core_targets, frozenset({*core_targets, *python_targets}), frozenset(package_roots)), + ("python", None): ( + frozenset({*bindings_targets, *python_targets}), + python_targets, + frozenset(package_roots), + ), + **{ + ("bindings", package.package_root): ( + frozenset( + { + ("bindings", package.package_root), + ("python", package.package_root), + } + ), + frozenset( + { + ("bindings", package.package_root), + ("core", package.cuda_variant), + ("python", package.package_root), + } + ), + frozenset({package.package_root}), + ) + for package in bindings_packages + }, + } + + source_changes: set[tuple[str, str | None]] = set() + test_changes: set[tuple[str, str | None]] = set() test_platforms: set[str] = set() - force_all = not merge_base or not baseline_run_id + release_package = bindings_config.match_tag(release_tag) if release_tag else None + if release_tag.startswith("v") and release_package is None: + raise ValueError(f"no configured CUDA bindings package root matches release tag: {release_tag!r}") + force_all = release_package is None and (bool(release_tag) or not merge_base or not baseline_run_id) - if not force_all: + if release_package is None and not force_all: for path in paths: path_parts = PurePosixPath(path).parts if not path_parts: @@ -99,9 +145,20 @@ def compute_workplan( if path_parts[0] == ".github" or path_parts[-1] in IGNORED_BASENAMES: continue - module = PACKAGE_MODULES.get(path_parts[0]) - if module is not None and len(path_parts) > 1: - relative = path_parts[1:] + target_match = next( + ( + (root, target) + for root, target in sorted( + package_targets.items(), key=lambda item: len(item[0].parts), reverse=True + ) + if len(path_parts) > len(root.parts) and path_parts[: len(root.parts)] == root.parts + ), + None, + ) + if target_match is not None: + root, target = target_match + module, _ = target + relative = path_parts[len(root.parts) :] if relative[0] == "docs": continue if ( @@ -109,16 +166,21 @@ def compute_workplan( or relative[0] == "examples" or (module == "core" and relative == ("pytest.ini",)) ): - test_changes.add(module) + if module == "core": + test_changes.update(core_targets) + elif module == "python": + test_changes.update(python_targets) + else: + test_changes.add(target) elif PurePosixPath(path).suffix in IGNORED_SUFFIXES and path not in linked_paths: continue else: - source_changes.add(module) + source_changes.add(target) continue is_test_path = any(part in {"test", "tests"} for part in path_parts[:-1]) if is_test_path: - test_changes.update(MODULES) + test_changes.update(all_targets) elif ( path in IGNORED_PATHS or PurePosixPath(path).suffix in IGNORED_SUFFIXES @@ -126,44 +188,93 @@ def compute_workplan( ): continue elif path_parts[0] in {"benchmarks", "cuda_python_test_helpers"}: - test_changes.update(MODULES) + test_changes.update(all_targets) else: force_all = True break - if force_all: - builds = set(MODULES) - tests = set(MODULES) + if release_package is not None: + builds = { + ("bindings", release_package.package_root), + ("python", release_package.package_root), + } + tests = set(builds) test_platforms = set(PLATFORMS) + sdist_package_roots = {release_package.package_root} + elif force_all: + builds = set(all_targets) + tests = set(all_targets) + test_platforms = set(PLATFORMS) + sdist_package_roots = set(package_roots) else: - builds: set[str] = set() - tests = set(MODULES) if test_platforms else set(test_changes) - for module in source_changes: - build_impact, test_impact = SOURCE_IMPACT[module] + builds: set[tuple[str, str | None]] = set() + tests = set(all_targets) if test_platforms else set(test_changes) + sdist_package_roots: set[str] = set() + for target in source_changes: + build_impact, test_impact, sdist_impact = source_impacts[target] builds.update(build_impact) tests.update(test_impact) + sdist_package_roots.update(sdist_impact) if source_changes or test_changes: test_platforms.update(PLATFORMS) - modules = { - module: { - "needs_build": module in builds, - "needs_test": module in tests, + def flags(module: str, selectors: tuple[str | None, ...]) -> dict[str, bool]: + return { + "needs_build": any((module, selector) in builds for selector in selectors), + "needs_test": any((module, selector) in tests for selector in selectors), + } + + modules: dict[str, dict[str, object]] = {"pathfinder": flags("pathfinder", (None,))} + for module in ("bindings", "python"): + package_decisions = { + package.package_root: flags(module, (package.package_root,)) for package in bindings_packages + } + modules[module] = { + **flags(module, package_roots), + "package_roots": package_decisions, } - for module in MODULES + + cuda_major_decisions = {variant: flags("core", (variant,)) for variant in cuda_variants} + modules["core"] = { + **flags("core", cuda_variants), + "cuda_majors": cuda_major_decisions, + } + + pathfinder_test = ("pathfinder", None) in tests + test_cuda_variants = { + package.cuda_variant + for package in bindings_packages + if pathfinder_test + or ("core", package.cuda_variant) in tests + or any((module, package.package_root) in tests for module in ("bindings", "python")) } + return { "modules": modules, + "sources": { + # Focused bindings releases intentionally omit unrelated artifacts. + "pathfinder": "published" if release_package is not None else "artifact", + }, "jobs": { # These gates cover both optional artifact builds and wheel tests. "platforms": {platform: platform in test_platforms for platform in PLATFORMS}, - "sdist_tests": bool(builds), - "core_api_checks": force_all or "core" in source_changes, + "sdist_tests": bool(sdist_package_roots), + "windows_arm64_build": (bool(baseline_run_id) and release_package is None) + or any( + int(package.cuda_major) >= 13 + and (("bindings", package.package_root) in builds or ("core", package.cuda_variant) in builds) + for package in bindings_packages + ), + "core_api_checks": force_all or ("core", None) in source_changes, + "test_cuda_majors": {variant: variant in test_cuda_variants for variant in cuda_variants}, + "sdist_package_roots": { + package_root: package_root in sdist_package_roots for package_root in package_roots + }, }, "merge_base": merge_base, "baseline": { - "run_id": baseline_run_id if not force_all else "", - "sha": merge_base if not force_all else "", + "run_id": baseline_run_id if release_package is None and not force_all else "", + "sha": merge_base if release_package is None and not force_all else "", }, } @@ -204,21 +315,57 @@ def _expand_linked_paths(paths: list[str], symlink_paths: list[str], *, root: Pa return expanded -def main() -> None: +def main(argv: list[str] | None = None) -> None: parser = argparse.ArgumentParser() parser.add_argument("--merge-base", default="") parser.add_argument("--baseline-run-id", default="") - args = parser.parse_args() + parser.add_argument("--release-tag", default="") + parser.add_argument( + "--github-output", + type=Path, + help="append normalized CI records to this GitHub output file", + ) + parser.add_argument( + "--github-step-summary", + type=Path, + help="append the workplan JSON to this GitHub step summary file", + ) + args = parser.parse_args(argv) + if args.github_step_summary is not None and args.github_output is None: + parser.error("--github-step-summary requires --github-output") - reusable_baseline = bool(args.merge_base and args.baseline_run_id) + config = load_config() + reusable_baseline = bool(args.merge_base and args.baseline_run_id and not args.release_tag) paths, linked_paths = _changed_paths(args.merge_base) if reusable_baseline else ([], set()) - plan = compute_workplan( - paths, - merge_base=args.merge_base, - baseline_run_id=args.baseline_run_id, - linked_paths=linked_paths, - ) - print(json.dumps(plan, separators=(",", ":"), sort_keys=True)) + try: + plan = compute_workplan( + bindings_config=config, + paths=paths, + linked_paths=linked_paths, + merge_base=args.merge_base, + baseline_run_id=args.baseline_run_id, + release_tag=args.release_tag, + ) + except ValueError as error: + parser.error(str(error)) + workplan = json.dumps(plan, separators=(",", ":"), sort_keys=True) + if args.github_output is None: + print(workplan) + return + + records = { + "bindings-config": config.to_json(), + "workplan": workplan, + } + with args.github_output.open("a", encoding="utf-8") as output: + for name, value in records.items(): + output.write(f"{name}={value}\n") + if args.github_step_summary is not None: + formatted = json.dumps(plan, indent=2, sort_keys=True) + with args.github_step_summary.open("a", encoding="utf-8") as summary: + summary.write( + f"### CI workplan\n\n
Workplan JSON\n\n```json\n{formatted}\n```\n
\n" + ) if __name__ == "__main__": diff --git a/ci/tools/env-vars b/ci/tools/env-vars index 30ff358ae98..c18a44521e5 100755 --- a/ci/tools/env-vars +++ b/ci/tools/env-vars @@ -8,159 +8,117 @@ set -euo pipefail -# Check if the script was called with exactly 1 argument -if [[ ${#} -ne 1 ]]; then - echo "Error: This script requires exactly 1 argument (the build mode). You provided ${#}" - echo "Usage: ${0} build_mode[build or test]" +if [[ ${#} -ne 1 || ( "${1}" != "build" && "${1}" != "test" ) ]]; then + echo "Usage: ${0} {build|test}" >&2 exit 1 fi - +BUILD_MODE=${1} PYTHON_VERSION_FORMATTED=$(echo "${PY_VER}" | tr -d '.') -CUDA_BINDINGS_ROOT="${CUDA_BINDINGS_ROOT:-cuda_bindings_13}" -BINDINGS_ARTIFACT_PREFIX="${BINDINGS_ARTIFACT_PREFIX:-cuda-bindings}" +CI_TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ "${HOST_PLATFORM}" == linux* ]]; then REPO_DIR=$(pwd) - TOOLS_PATH="${REPO_DIR}/ci/tools" + TOOLS_PATH="${CI_TOOLS_DIR}" elif [[ "${HOST_PLATFORM}" == win* ]]; then - PWD=$(pwd) - REPO_DIR=$(cygpath -w ${PWD}) - TOOLS_PATH=$(cygpath -w ${PWD}/ci/tools) + REPO_DIR=$(cygpath -w "$(pwd)") + TOOLS_PATH=$(cygpath -w "${CI_TOOLS_DIR}") fi -echo "${TOOLS_PATH}" >> $GITHUB_PATH -echo "CUDA_PYTHON_PARALLEL_LEVEL=$(nproc)" >> $GITHUB_ENV +echo "${TOOLS_PATH}" >> "$GITHUB_PATH" +echo "CUDA_PYTHON_PARALLEL_LEVEL=$(nproc)" >> "$GITHUB_ENV" CUDA_CORE_ARTIFACT_BASENAME="cuda-core-python${PYTHON_VERSION_FORMATTED}-${HOST_PLATFORM}" +CUDA_CORE_ARTIFACT_NAME="${CUDA_CORE_ARTIFACT_BASENAME}-${SHA}" { echo "CUDA_CORE_ARTIFACT_BASENAME=${CUDA_CORE_ARTIFACT_BASENAME}" - echo "CUDA_CORE_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_BASENAME}-${SHA}" + echo "CUDA_CORE_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_NAME}" echo "CUDA_CORE_ARTIFACTS_DIR=$(realpath "${REPO_DIR}/cuda_core/dist")" echo "CUDA_CORE_CYTHON_TESTS_DIR=$(realpath "${REPO_DIR}/cuda_core/tests/cython")" echo "CUDA_CORE_TEST_BINARIES_DIR=$(realpath "${REPO_DIR}/cuda_core/tests/test_binaries")" echo "PYTHON_VERSION_FORMATTED=${PYTHON_VERSION_FORMATTED}" -} >> $GITHUB_ENV +} >> "$GITHUB_ENV" -if [[ "${1}" == "build" ]]; then - if [[ "${HOST_PLATFORM}" == "linux-64" ]]; then - CIBW_PLATFORM_TAG="manylinux_x86_64" - elif [[ "${HOST_PLATFORM}" == "linux-aarch64" ]]; then - CIBW_PLATFORM_TAG="manylinux_aarch64" - elif [[ "${HOST_PLATFORM}" == "win-64" ]]; then - CIBW_PLATFORM_TAG="win_amd64" - elif [[ "${HOST_PLATFORM}" == "win-arm64" ]]; then - CIBW_PLATFORM_TAG="win_arm64" - else - echo "Error: unsupported HOST_PLATFORM=${HOST_PLATFORM}" >&2 - exit 1 - fi +if [[ "${BUILD_MODE}" == "build" ]]; then + case "${HOST_PLATFORM}" in + linux-64) CIBW_PLATFORM_TAG="manylinux_x86_64" ;; + linux-aarch64) CIBW_PLATFORM_TAG="manylinux_aarch64" ;; + win-64) CIBW_PLATFORM_TAG="win_amd64" ;; + win-arm64) CIBW_PLATFORM_TAG="win_arm64" ;; + *) echo "Error: unsupported HOST_PLATFORM=${HOST_PLATFORM}" >&2; exit 1 ;; + esac echo "CIBW_BUILD=cp${PYTHON_VERSION_FORMATTED}-${CIBW_PLATFORM_TAG}" >> "$GITHUB_ENV" - BUILD_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${CUDA_VER})" - echo "BUILD_CUDA_MAJOR=${BUILD_CUDA_MAJOR}" >> $GITHUB_ENV - echo "BUILD_PREV_CUDA_MAJOR=$((${BUILD_CUDA_MAJOR} - 1))" >> $GITHUB_ENV - CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${CUDA_VER}-${HOST_PLATFORM}" - maintenance_package=$(python -m ci.tools.bindings_config --release-status maintenance) - maintenance_root=$(jq -r '.package_root' <<< "$maintenance_package") - maintenance_version=$(jq -r '.toolkit_version' <<< "$maintenance_package") - { - echo "MAINTENANCE_BINDINGS_ROOT=${maintenance_root}" - echo "MAINTENANCE_CUDA_VERSION=${maintenance_version}" - echo "MAINTENANCE_CUDA_MAJOR=${maintenance_version%%.*}" - echo "MAINTENANCE_BINDINGS_ARTIFACTS_DIR=$(realpath "${REPO_DIR}/${maintenance_root}/dist")" - echo "MAINTENANCE_BINDINGS_CYTHON_TESTS_DIR=$(realpath "${REPO_DIR}/${maintenance_root}/tests/cython")" - echo "MAINTENANCE_BINDINGS_ARTIFACT_NAME=cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${maintenance_version}-${HOST_PLATFORM}-${SHA}" - } >> "$GITHUB_ENV" -elif [[ "${1}" == "test" ]]; then - BUILD_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${BUILD_CUDA_VER})" - TEST_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${CUDA_VER})" - CUDA_BINDINGS_ARTIFACT_BASENAME="${BINDINGS_ARTIFACT_PREFIX}-python${PYTHON_VERSION_FORMATTED}-cuda${BUILD_CUDA_VER}-${HOST_PLATFORM}" + python -m ci.tools.bindings_config write-build-env "$GITHUB_ENV" \ + --repo-root "$REPO_DIR" --python-version "$PY_VER" \ + --host-platform "$HOST_PLATFORM" --sha "$SHA" +else + TEST_CUDA_MAJOR="${CUDA_VER%%.*}" + TEST_CUDA_MINOR="$(cut -d '.' -f 2 <<< "${CUDA_VER}")" + PATHFINDER_SOURCE="${PATHFINDER_SOURCE:-artifact}" + SKIP_CUDA_BINDINGS_TEST=1 + SKIP_CYTHON_TEST=1 - # BINDINGS_SOURCE controls which cuda-bindings to install at test time: - # main — use the just-built bindings wheel from this CI run - # backport — fetch bindings from the prior (N-1) branch - # floor — install from PyPI the oldest cuda-bindings that the cuda-core - # wheel under test supports, its per-major floor (see - # cuda_core/cuda/core/_bindings_floor.py and ci/tools/run-tests). - # Selected when the test CTK minor differs from the minor the - # wheel was built against. Those rows exercise a new cuda-core - # with the floor cuda-bindings and older CTK libraries, the skew - # that cuda-core supports. cuda-bindings older than the floor is - # unsupported and fails at import - # (https://github.com/NVIDIA/cuda-python/issues/2783). - # - # SKIP_CUDA_BINDINGS_TEST / SKIP_CYTHON_TEST control which *tests* to run - # (they do NOT affect installation — that's BINDINGS_SOURCE's job). - - BUILD_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_CUDA_VER})" - TEST_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${CUDA_VER})" - # CI builds the prior-major half of the cuda-core wheel against the prev_build - # toolkit in ci/versions.yml and the backport branch's bindings. - if ! command -v yq >/dev/null 2>&1; then - echo "Error: yq is required to read ci/versions.yml" >&2 - exit 1 - fi - BUILD_PREV_CUDA_VER="$(yq -r '.cuda.prev_build.version' ci/versions.yml)" - BUILD_PREV_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_PREV_CUDA_VER})" - - if [[ -n "${BINDINGS_SOURCE:-}" ]]; then - SKIP_CUDA_BINDINGS_TEST=1 - SKIP_CYTHON_TEST=1 - if [[ "$BINDINGS_SOURCE" == main ]]; then - SKIP_CUDA_BINDINGS_TEST="${SKIP_BINDINGS_TEST_OVERRIDE:-0}" - SKIP_CYTHON_TEST=0 + if [[ -z "${BINDINGS_CONFIG:-}" ]]; then + # Old snapshots use main/backport/floor and generic13 artifacts until7. + : "${BUILD_CUDA_VER:?BUILD_CUDA_VER is required for legacy snapshot routing}" + CUDA_BINDINGS_ROOT=cuda_bindings_13 + if [[ ! -d "${CUDA_BINDINGS_ROOT}" ]]; then + CUDA_BINDINGS_ROOT=cuda_bindings fi - elif [[ ${BUILD_CUDA_MAJOR} != ${TEST_CUDA_MAJOR} ]]; then - SKIP_CUDA_BINDINGS_TEST=1 - SKIP_CYTHON_TEST=1 - if [[ ${BUILD_PREV_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then - # Prior major, minor mismatch (e.g. built against 12.9, test=12.6): floor - # bindings from PyPI with the older CTK libraries. + BINDINGS_BUILD_CUDA_VER="${BUILD_CUDA_VER}" + build_minor="$(cut -d '.' -f 2 <<< "${BUILD_CUDA_VER}")" + previous_minor="$(yq -r '.cuda.prev_build.version' ci/versions.yml | cut -d '.' -f 2)" + if [[ "${BUILD_CUDA_VER%%.*}" != "${TEST_CUDA_MAJOR}" ]]; then + BINDINGS_SOURCE=backport + if [[ "${previous_minor}" != "${TEST_CUDA_MINOR}" ]]; then + BINDINGS_SOURCE=floor + fi + elif [[ "${build_minor}" != "${TEST_CUDA_MINOR}" ]]; then BINDINGS_SOURCE=floor else - # Prior major, same minor (e.g. build=13.x, test=12.9): the backport branch. - BINDINGS_SOURCE=backport + BINDINGS_SOURCE=main + SKIP_CUDA_BINDINGS_TEST="${SKIP_BINDINGS_TEST_OVERRIDE:-0}" + SKIP_CYTHON_TEST=0 fi - elif [[ ${BUILD_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then - # Same major, minor mismatch (e.g. build=13.4, test=13.0): floor bindings - # from PyPI with the older CTK libraries. - BINDINGS_SOURCE=floor - SKIP_CUDA_BINDINGS_TEST=1 - SKIP_CYTHON_TEST=1 + CUDA_PYTHON_ARTIFACT_NAME=cuda-python-wheel + core_cython_suffix=-tests else - # Exact match: use the just-built bindings wheel. - BINDINGS_SOURCE=main - if [[ "${SKIP_BINDINGS_TEST_OVERRIDE:-0}" == "1" ]]; then - SKIP_CUDA_BINDINGS_TEST=1 + core_cython_suffix="-cu${TEST_CUDA_MAJOR}-tests" + if [[ "${BINDINGS_SOURCE}" == local ]]; then + CUDA_BINDINGS_ROOT="${BINDINGS_SOURCE_DIR}" + BINDINGS_BUILD_CUDA_VER="${CUDA_VER}" + SKIP_CUDA_BINDINGS_TEST="${SKIP_BINDINGS_TEST_OVERRIDE:-0}" + SKIP_CYTHON_TEST=0 + elif [[ "${BINDINGS_SOURCE}" == floor ]]; then + CUDA_BINDINGS_ROOT="${DEFAULT_BINDINGS_SOURCE_DIR}" + BINDINGS_BUILD_CUDA_VER="${CUDA_VER}" else - SKIP_CUDA_BINDINGS_TEST=0 + echo "Error: BINDINGS_SOURCE must be local or floor, got ${BINDINGS_SOURCE}" >&2 + exit 1 fi - SKIP_CYTHON_TEST=0 + CUDA_PYTHON_ARTIFACT_NAME="cuda-python-wheel-cuda${BINDINGS_BUILD_CUDA_VER}" fi - # We don't test compute-sanitizer on CTK<12 because backporting fixes is too much effort - # We only test compute-sanitizer on python 3.12 arbitrarily; we don't need to use sanitizer on the entire matrix - # Only local ctk installs have compute-sanitizer; there is no wheel for it + + CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${BINDINGS_BUILD_CUDA_VER}-${HOST_PLATFORM}" if [[ "${PY_VER}" == "3.12" && "${CUDA_VER}" != "11.8.0" && "${LOCAL_CTK}" == 1 && "${HOST_PLATFORM}" == linux* ]]; then - echo "LATEST_CUDA_VERSION=$(bash ci/tools/guess_latest.sh $TEST_CUDA_MAJOR)" >> $GITHUB_ENV + echo "LATEST_CUDA_VERSION=$(bash "${CI_TOOLS_DIR}/guess_latest.sh" "$TEST_CUDA_MAJOR")" >> "$GITHUB_ENV" SETUP_SANITIZER=1 else SETUP_SANITIZER=0 - echo "SANITIZER_CMD=" >> $GITHUB_ENV + echo "SANITIZER_CMD=" >> "$GITHUB_ENV" fi { echo "SETUP_SANITIZER=${SETUP_SANITIZER}" echo "BINDINGS_SOURCE=${BINDINGS_SOURCE}" + echo "CUDA_BINDINGS_ROOT=${CUDA_BINDINGS_ROOT}" + echo "PATHFINDER_SOURCE=${PATHFINDER_SOURCE}" + echo "CUDA_PYTHON_ARTIFACT_NAME=${CUDA_PYTHON_ARTIFACT_NAME}" echo "SKIP_CUDA_BINDINGS_TEST=${SKIP_CUDA_BINDINGS_TEST}" echo "SKIP_CYTHON_TEST=${SKIP_CYTHON_TEST}" echo "TEST_CUDA_MAJOR=${TEST_CUDA_MAJOR}" echo "TEST_CUDA_MINOR=${TEST_CUDA_MINOR}" - } >> $GITHUB_ENV + echo "CUDA_BINDINGS_ARTIFACT_BASENAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}" + echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${SHA}" + echo "CUDA_BINDINGS_ARTIFACTS_DIR=$(realpath "${REPO_DIR}/${CUDA_BINDINGS_ROOT}/dist")" + echo "CUDA_BINDINGS_CYTHON_TESTS_DIR=$(realpath "${REPO_DIR}/${CUDA_BINDINGS_ROOT}/tests/cython")" + echo "CUDA_CORE_CYTHON_TEST_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_NAME}${core_cython_suffix}" + } >> "$GITHUB_ENV" fi - -{ - echo "CUDA_BINDINGS_ROOT=${CUDA_BINDINGS_ROOT}" - echo "CUDA_BINDINGS_ARTIFACT_BASENAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}" - echo "CUDA_BINDINGS_ARTIFACT_NAME=${CUDA_BINDINGS_ARTIFACT_BASENAME}-${SHA}" - echo "CUDA_BINDINGS_ARTIFACTS_DIR=$(realpath "${REPO_DIR}/${CUDA_BINDINGS_ROOT}/dist")" - echo "CUDA_BINDINGS_CYTHON_TESTS_DIR=$(realpath "${REPO_DIR}/${CUDA_BINDINGS_ROOT}/tests/cython")" - echo "CUDA_CORE_CYTHON_TEST_ARTIFACT_NAME=${CUDA_CORE_ARTIFACT_BASENAME}-${SHA}${CORE_CYTHON_SUFFIX:--tests}" - echo "CUDA_PYTHON_ARTIFACT_NAME=${CUDA_PYTHON_ARTIFACT_NAME:-cuda-python-wheel}" -} >> $GITHUB_ENV diff --git a/ci/tools/run-tests b/ci/tools/run-tests index 1e92950e8dc..92fa084cb2c 100755 --- a/ci/tools/run-tests +++ b/ci/tools/run-tests @@ -49,8 +49,21 @@ fi # all wheels together in a single pip call further below. if [[ "${test_module}" != nightly-* ]]; then pushd ./cuda_pathfinder - echo "Installing pathfinder wheel" - pip install ./*.whl --group test "${TEST_FT_GROUP[@]}" + if [[ "${PATHFINDER_SOURCE:-}" == "artifact" ]]; then + echo "Installing pathfinder wheel from CI artifacts" + pathfinder_wheels=(./*.whl) + if [[ "${#pathfinder_wheels[@]}" -ne 1 || ! -f "${pathfinder_wheels[0]}" ]]; then + echo "Expected exactly one cuda-pathfinder wheel in $(pwd)" >&2 + exit 1 + fi + pip install "${pathfinder_wheels[0]}" --group test "${TEST_FT_GROUP[@]}" + elif [[ "${PATHFINDER_SOURCE:-}" == "published" && "${test_module}" == "bindings" ]]; then + echo "Installing published pathfinder for a focused bindings release" + pip install cuda-pathfinder --group test "${TEST_FT_GROUP[@]}" + else + echo "Error: PATHFINDER_SOURCE must be artifact, or published for bindings release tests; got ${PATHFINDER_SOURCE:-}" >&2 + exit 1 + fi popd fi @@ -68,16 +81,15 @@ if [[ "${test_module}" == "pathfinder" ]]; then popd elif [[ "${test_module}" == "bindings" ]]; then echo "Installing bindings wheel" - pushd "./${CUDA_BINDINGS_ROOT:-cuda_bindings_13}" - if [[ "${LOCAL_CTK}" == 1 ]]; then - pip install "${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl --group test "${TEST_FT_GROUP[@]}" - else + pushd "./${CUDA_BINDINGS_ROOT}" + bindings_requirements=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl) + bindings_toolkit_requirement=() + if [[ "${LOCAL_CTK}" != 1 ]]; then TEST_CUDA_MAJOR_MINOR="$(cut -d '.' -f 1-2 <<< "${CUDA_VER}")" - # Keep the wheel-installed Toolkit aligned with the matrix-selected minor; - # newer minors can grow ABI structs beyond the size compiled into this wheel. - pip install $(ls "${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl)[all] --group test "${TEST_FT_GROUP[@]}" \ - "cuda-toolkit==${TEST_CUDA_MAJOR_MINOR}.*" + bindings_requirements=("${bindings_requirements[@]/%/[all]}") + bindings_toolkit_requirement=("cuda-toolkit==${TEST_CUDA_MAJOR_MINOR}.*") fi + pip install "${bindings_requirements[@]}" --group test "${TEST_FT_GROUP[@]}" "${bindings_toolkit_requirement[@]}" echo "Running bindings tests" ${SANITIZER_CMD} pytest -rsxXfE -v --randomly-dont-reorganize --exclude-warning-annotations "${PYTEST_PARALLEL_ARGS[@]}" --junitxml "${TEST_RESULTS_DIR}/junit-bindings.xml" tests/ | fold_pytest_log if [[ "${SKIP_CYTHON_TEST}" == 0 ]]; then @@ -86,17 +98,17 @@ elif [[ "${test_module}" == "bindings" ]]; then popd elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then # Shared setup for core and nightly modes. - TEST_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${CUDA_VER})" + TEST_CUDA_MAJOR="$(cut -d '.' -f 1 <<< "${CUDA_VER}")" TEST_CUDA_MAJOR_MINOR="$(cut -d '.' -f 1-2 <<< "${CUDA_VER}")" # Resolve bindings based on BINDINGS_SOURCE (set by env-vars): - # main/backport → local wheel from artifacts dir - # floor → the oldest cuda-bindings that the cuda-core wheel under test - # supports, read from that wheel and installed from PyPI + # local → matching configured-package wheel from this run + # floor -> the oldest supported bindings, read from the Core wheel + # and installed from PyPI BINDINGS_ARGS=() if [[ "${BINDINGS_SOURCE}" == "floor" ]]; then CORE_WHL_FOR_FLOOR=("${CUDA_CORE_ARTIFACTS_DIR}"/*.whl) - BINDINGS_FLOOR="$(python ci/tools/cuda_core_bindings_floor.py --wheel "${CORE_WHL_FOR_FLOOR[0]}" --major "${TEST_CUDA_MAJOR}")" + BINDINGS_FLOOR="$(python "${CI_TOOLS_DIR}/cuda_core_bindings_floor.py" --wheel "${CORE_WHL_FOR_FLOOR[0]}" --major "${TEST_CUDA_MAJOR}")" echo "cuda-bindings floor of ${CORE_WHL_FOR_FLOOR[0]##*/} for CUDA ${TEST_CUDA_MAJOR}: ${BINDINGS_FLOOR}" BINDINGS_ARGS+=("cuda-bindings==${BINDINGS_FLOOR}") else @@ -114,10 +126,19 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then fi if [[ "${test_module}" == nightly-* ]]; then + if [[ "${PATHFINDER_SOURCE:-}" != "artifact" ]]; then + echo "Error: nightly tests require PATHFINDER_SOURCE=artifact" >&2 + exit 1 + fi # Resolve pathfinder wheel to absolute path before pushd. # CUDA_BINDINGS_ARTIFACTS_DIR and CUDA_CORE_ARTIFACTS_DIR are already # absolute (set via realpath in env-vars). - PATHFINDER_WHL=($(realpath ./cuda_pathfinder/*.whl)) + pathfinder_wheels=(./cuda_pathfinder/*.whl) + if [[ "${#pathfinder_wheels[@]}" -ne 1 || ! -f "${pathfinder_wheels[0]}" ]]; then + echo "Expected exactly one cuda-pathfinder wheel in ./cuda_pathfinder" >&2 + exit 1 + fi + PATHFINDER_WHL=("$(realpath "${pathfinder_wheels[0]}")") fi if [[ "${test_module}" == "core" ]]; then @@ -143,8 +164,8 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then pip list echo "Running core tests" ${SANITIZER_CMD} pytest -rsxXfE -v --randomly-dont-reorganize --exclude-warning-annotations "${PYTEST_PARALLEL_ARGS[@]}" --junitxml "${TEST_RESULTS_DIR}/junit-core.xml" tests/ | fold_pytest_log - # Currently our CI always installs the latest bindings (from either major version). - # This is not compatible with the test requirements. + # Compiled Cython tests require the bindings used to build them. Older + # toolkit rows intentionally exercise published floor bindings instead. if [[ "${SKIP_CYTHON_TEST}" == 0 ]]; then ${SANITIZER_CMD} pytest -rsxXfE -v --randomly-dont-reorganize --exclude-warning-annotations "${PYTEST_PARALLEL_ARGS[@]}" --junitxml "${TEST_RESULTS_DIR}/junit-core-cython.xml" tests/cython | fold_pytest_log fi diff --git a/ci/tools/select_test_matrix.py b/ci/tools/select_test_matrix.py new file mode 100644 index 00000000000..e9cf82d243c --- /dev/null +++ b/ci/tools/select_test_matrix.py @@ -0,0 +1,113 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Select wheel-test rows and their local or published bindings sources.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from typing import Any + + +def select_test_matrix( + rows: list[dict[str, Any]], + packages: list[dict[str, Any]], + *, + workplan: dict[str, Any] | None = None, + build_type: str = "pull-request", + test_mode: str = "standard", + platform: str = "linux", +) -> dict[str, list[dict[str, Any]]]: + """Apply the workplan after the caller's optional jq matrix filter.""" + current_root = next(package["package_root"] for package in packages if package["release_status"] == "current") + enabled = ( + {package["cuda_variant"]: True for package in packages} + if workplan is None + else workplan["jobs"]["test_cuda_majors"] + ) + modules = None if workplan is None else workplan["modules"] + selected = [] + for source_row in rows: + major = source_row["CUDA_VER"].split(".")[0] + variant = f"cu{major}" + if not enabled.get(variant, False): + continue + package = next((package for package in packages if package["toolkit_version"] == source_row["CUDA_VER"]), None) + root = "" if package is None else package["package_root"] + suites = [] + if test_mode != "standard": + suites.append(test_mode) + else: + if modules is None or modules["pathfinder"]["needs_test"]: + suites.append("pathfinder") + if package is not None and (modules is None or modules["bindings"]["package_roots"][root]["needs_test"]): + suites.append(f"bindings{major}") + if modules is None or modules["core"]["cuda_majors"][variant]["needs_test"]: + suites.append(f"core{major}") + if package is not None and (modules is None or modules["python"]["package_roots"][root]["needs_test"]): + suites.append(f"meta{major}") + if not suites: + continue + driver = source_row["DRIVER"] + # Custom versions are installed after scheduling on a latest-driver + # runner. WSL cannot perform that driver swap. + runner_driver = driver if driver in ("latest", "earliest") else "latest" + if platform == "linux" and runner_driver != driver and source_row.get("FLAVOR") == "wsl": + raise ValueError("custom DRIVER is not supported with FLAVOR=wsl") + selected.append( + { + **source_row, + "CUDA_VARIANT": variant, + "BINDINGS_SOURCE": "floor" if package is None else "local", + "BINDINGS_SOURCE_DIR": root, + "DEFAULT_BINDINGS_SOURCE_DIR": current_root, + "PATHFINDER_SOURCE": "artifact" if workplan is None else workplan["sources"]["pathfinder"], + "SUITES": "+".join(suites), + "RUNNER_DRIVER": runner_driver, + } + ) + if not selected: + raise ValueError("Empty matrix after CUDA-major workplan filtering") + # Nightly integrations may deliberately use published bindings with older + # toolkits. Ordinary PRs must not silently lose a required local-wheel row. + if test_mode == "standard" and build_type == "pull-request": + covered = {row["BINDINGS_SOURCE_DIR"] for row in selected if row["BINDINGS_SOURCE"] == "local"} + missing = [ + package["package_root"] + for package in packages + if enabled.get(package["cuda_variant"], False) + and ( + modules is None + or modules["bindings"]["package_roots"][package["package_root"]]["needs_test"] + or modules["python"]["package_roots"][package["package_root"]]["needs_test"] + ) + and package["package_root"] not in covered + ] + if missing: + raise ValueError(f"filtered test matrix omits required local-wheel coverage: {', '.join(missing)}") + return {"include": selected} + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--platform", choices=("linux", "windows"), required=True) + args = parser.parse_args() + try: + result = select_test_matrix( + json.load(sys.stdin), + json.loads(os.environ["BINDINGS_CONFIG"])["package_roots"], + workplan=json.loads(os.environ.get("WORKPLAN") or "null"), + build_type=os.environ["BUILD_TYPE"], + test_mode=os.environ["TEST_MODE"], + platform=args.platform, + ) + except ValueError as error: + parser.error(str(error)) + print(json.dumps(result, separators=(",", ":"))) + + +if __name__ == "__main__": + main() diff --git a/ci/tools/tests/AGENTS.md b/ci/tools/tests/AGENTS.md new file mode 100644 index 00000000000..86b231c76f3 --- /dev/null +++ b/ci/tools/tests/AGENTS.md @@ -0,0 +1,20 @@ +# CI Tool Tests + +These instructions apply only to tests under `ci/tools/tests/`. Package and +runtime tests follow their own guidance. + +Keep a test only when wrong behavior would be silent: nothing would fail +loudly in CI, but the result would be wrong. Cut a test when a mistake already +produces an immediate, obvious CI failure. This includes environment-variable +wiring, wheel counts, argument validation, exact pip command lines, and the +schema of a checked-in file that every CI run loads. + +Useful outcome coverage includes skipped required tests, wrong release +artifacts, incorrect version milestones, missing Core ABI variants, and +silently lost local-wheel matrix coverage. Assert those outcomes rather than +workflow text, step names, cosmetic labels, or intermediate command order. + +Use self-contained metadata fixtures so routine version and dependency bumps +do not break unrelated tests. If a test intentionally uses the live registry, +derive its toolkit and package values from that registry. Keep fixtures and +parameter sets as small as the distinct silent-failure risks allow. diff --git a/ci/tools/tests/test_bindings_config.py b/ci/tools/tests/test_bindings_config.py index 66e302cc6a3..498411060a1 100644 --- a/ci/tools/tests/test_bindings_config.py +++ b/ci/tools/tests/test_bindings_config.py @@ -17,7 +17,6 @@ from ci.tools import check_pixi_cuda_version from ci.tools.bindings_config import BindingsPackage, load_config, main -from ci.tools.compute_ci_plan import compute_workplan ROOT = Path(__file__).resolve().parents[3] @@ -114,18 +113,6 @@ def test_pixi_check_rejects_wrong_maintenance_major_and_broad_current_pin( assert f"{package_root}/pixi.toml" in capsys.readouterr().err -@pytest.mark.agent_authored(model="gpt-6") -def test_cuda12_source_changes_select_bindings_consumers(): - plan = compute_workplan( - ["cuda_bindings_12/cuda/bindings/_internal/utils.pyx"], merge_base="base", baseline_run_id="123" - ) - assert {name for name, decision in plan["modules"].items() if decision["needs_build"]} == { - "bindings", - "core", - "python", - } - - @pytest.mark.agent_authored(model="gpt-6") def test_cuda12_milestone_does_not_change_current_package_versions(tmp_path, monkeypatch): current = load_config(ROOT / "ci" / "versions.yml").package_for_release_status("current") diff --git a/ci/tools/tests/test_compute_ci_plan.py b/ci/tools/tests/test_compute_ci_plan.py index b87326420ee..8c2de8e34e3 100644 --- a/ci/tools/tests/test_compute_ci_plan.py +++ b/ci/tools/tests/test_compute_ci_plan.py @@ -1,184 +1,177 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations +"""Protect CI selection outcomes that could otherwise silently omit coverage.""" -import tempfile -import unittest +from dataclasses import replace from pathlib import Path +import pytest + +from ci.tools.bindings_config import load_config from ci.tools.compute_ci_plan import _expand_linked_paths, compute_workplan -ALL_MODULES = {"pathfinder", "bindings", "core", "python"} -ALL_PLATFORMS = {"linux", "windows"} +CONFIG = load_config() +PACKAGES = CONFIG.package_roots +ROOTS = {package.package_root for package in PACKAGES} +VARIANTS = {package.cuda_variant for package in PACKAGES} +BINDINGS = {("bindings", root) for root in ROOTS} +CORE = {("core", variant) for variant in VARIANTS} +META = {("python", root) for root in ROOTS} +ALL = {("pathfinder", None), *BINDINGS, *CORE, *META} -def plan_for( - *paths: str, - baseline: bool = True, - linked_paths: set[str] | None = None, -) -> dict[str, object]: +def plan_for(*paths, baseline=True, release_tag="", config=CONFIG, linked_paths=()): return compute_workplan( - list(paths), + bindings_config=config, + paths=list(paths), + linked_paths=set(linked_paths), merge_base="base", baseline_run_id="123" if baseline else "", - linked_paths=linked_paths, + release_tag=release_tag, ) -def selected(plan: dict[str, object], key: str) -> set[str]: - modules = plan["modules"] - assert isinstance(modules, dict) - return {name for name, decision in modules.items() if decision[key]} - - -def selected_platforms(plan: dict[str, object]) -> set[str]: - jobs = plan["jobs"] - assert isinstance(jobs, dict) - platforms = jobs["platforms"] - assert isinstance(platforms, dict) - assert set(platforms) == ALL_PLATFORMS - return {name for name, enabled in platforms.items() if enabled} - - -class ComputeWorkplanTest(unittest.TestCase): - def test_path_impacts(self) -> None: - cases = { - "cuda_pathfinder/cuda/pathfinder/_loader.py": (ALL_MODULES, ALL_MODULES, False), - "cuda_bindings_13/cuda/bindings/driver.pyx": ( - {"bindings", "core", "python"}, - {"bindings", "core", "python"}, - False, - ), - "cuda_core/cuda/core/_device.py": ({"core"}, {"core", "python"}, True), - "cuda_core/cuda/core/examples/demo.py": ({"core"}, {"core", "python"}, True), - "cuda_python/pyproject.toml": ({"bindings", "python"}, {"python"}, False), - "cuda_pathfinder/tests/test_loader.py": (set(), {"pathfinder"}, False), - "cuda_bindings_13/examples/0_Introduction/vectorAddDrv.py": (set(), {"bindings"}, False), - "cuda_bindings_13/tests/README.md": (set(), {"bindings"}, False), - "cuda_core/pytest.ini": (set(), {"core"}, False), - "cuda_python/tests/test_import.py": (set(), {"python"}, False), - "cuda_python_test_helpers/cuda_python_test_helpers/cuda_utils.py": ( - set(), - ALL_MODULES, - False, - ), - "benchmarks/cuda_bindings/run_pyperf.py": (set(), ALL_MODULES, False), - "benchmarks/cuda_core/runner.py": (set(), ALL_MODULES, False), - "ci/tools/run-tests": (ALL_MODULES, ALL_MODULES, True), - "ci/versions.yml": (ALL_MODULES, ALL_MODULES, True), - "pytest.ini": (ALL_MODULES, ALL_MODULES, True), - } - - for path, (builds, tests, core_api) in cases.items(): - with self.subTest(path=path): - plan = plan_for(path) - assert selected(plan, "needs_build") == builds - assert selected(plan, "needs_test") == tests - assert selected_platforms(plan) == ALL_PLATFORMS - assert plan["jobs"]["sdist_tests"] == bool(builds) - assert plan["jobs"]["core_api_checks"] == core_api - - def test_test_infrastructure_platforms(self) -> None: - cases = { - ".github/workflows/test-wheel-linux.yml": {"linux"}, - ".github/workflows/test-wheel-windows.yml": {"windows"}, - "ci/tools/configure_driver_mode.ps1": {"windows"}, - "ci/tools/guess_latest.sh": {"linux"}, - "ci/tools/install_gpu_driver.ps1": {"windows"}, - "ci/tools/install_gpu_driver.sh": {"linux"}, - "ci/tools/setup-sanitizer": {"linux"}, - } - - for path, platforms in cases.items(): - with self.subTest(path=path): - plan = plan_for(path) - assert not selected(plan, "needs_build") - assert selected(plan, "needs_test") == ALL_MODULES - assert selected_platforms(plan) == platforms - assert not plan["jobs"]["sdist_tests"] - assert not plan["jobs"]["core_api_checks"] - - mixed_plan = plan_for("ci/tools/install_gpu_driver.sh", "ci/tools/install_gpu_driver.ps1") - assert selected_platforms(mixed_plan) == ALL_PLATFORMS - - source_plan = plan_for("ci/tools/install_gpu_driver.sh", "cuda_python/pyproject.toml") - assert selected_platforms(source_plan) == ALL_PLATFORMS - - def test_ignored_paths_select_no_work(self) -> None: - for path in ( - "cuda_core/docs/index.rst", - "cuda_core/pixi.toml", - "cuda_core/tests/fixtures/pixi.toml", - "benchmarks/cuda_bindings/pixi.toml", - "benchmarks/cuda_bindings/AGENTS.md", - "cuda_core/cuda/core/_cpp/rt/DESIGN.md", - "cuda_bindings_13/README.md", - "cuda_core/README.md", - "new-area/pixi.toml", - "notes.md", - "diagram.svg", - ".github/labeler.yml", - ".github/ISSUE_TEMPLATE/bug.yml", - ".github/workflows/runner-trust-probe.yml", - "ci/tools/runner_trust_probe.sh", - "ci/tools/runner_trust_probe.ps1", - "ci/tools/runner_trust_probe_node.js", - ): - with self.subTest(path=path): - plan = plan_for(path) - assert not selected(plan, "needs_build") - assert not selected(plan, "needs_test") - assert not selected_platforms(plan) - - def test_unknown_path_and_missing_baseline_force_all(self) -> None: - for plan in ( - plan_for("new-top-level-file"), - plan_for("new-area/config.toml"), - plan_for(".github/workflows/new-main-ci-workflow.yml"), - plan_for(".github/actions/doc_preview/action.yml"), - plan_for("cuda_core/docs/index.rst", baseline=False), - compute_workplan([], merge_base="", baseline_run_id="123"), - ): - assert selected(plan, "needs_build") == ALL_MODULES - assert selected(plan, "needs_test") == ALL_MODULES - assert selected_platforms(plan) == ALL_PLATFORMS - assert plan["jobs"]["core_api_checks"] - assert plan["baseline"] == {"run_id": "", "sha": ""} - - def test_mixed_changes_are_combined(self) -> None: - plan = plan_for("cuda_core/tests/test_device.py", "cuda_python/pyproject.toml") - assert selected(plan, "needs_build") == {"bindings", "python"} - assert selected(plan, "needs_test") == {"core", "python"} - assert selected_platforms(plan) == ALL_PLATFORMS - assert plan["jobs"]["sdist_tests"] - assert plan["baseline"] == {"run_id": "123", "sha": "base"} - - def test_changed_symlink_targets_include_their_consumers(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - (root / "cuda_python").mkdir() - (root / "cuda_core").mkdir() - (root / "README.md").write_text("readme", encoding="utf-8") - (root / "cuda_python" / "README.md").symlink_to("../README.md") - (root / "cuda_core" / "README.md").symlink_to("../README.md") - - paths = _expand_linked_paths( - ["README.md"], - ["cuda_python/README.md"], - root=root, - ) - - assert paths == ["README.md", "cuda_python/README.md"] - plan = plan_for(*paths, linked_paths={"cuda_python/README.md"}) - assert selected(plan, "needs_build") == {"bindings", "python"} - assert selected(plan, "needs_test") == {"python"} - - removed_link = plan_for("cuda_python/README.md", linked_paths={"cuda_python/README.md"}) - assert selected(removed_link, "needs_build") == {"bindings", "python"} - assert selected(removed_link, "needs_test") == {"python"} - - -if __name__ == "__main__": - unittest.main() +def selected(plan, decision): + targets = set() + for component, details in plan["modules"].items(): + if component == "pathfinder": + if details[decision]: + targets.add((component, None)) + continue + selectors = details["cuda_majors" if component == "core" else "package_roots"] + targets.update((component, selector) for selector, flags in selectors.items() if flags[decision]) + return targets + + +@pytest.mark.parametrize( + ("path", "builds", "tests"), + [ + ("cuda_pathfinder/cuda/pathfinder/_loader.py", ALL, ALL), + ("cuda_core/cuda/core/_device.py", CORE, CORE | META), + ("cuda_python/setup.py", BINDINGS | META, META), + ("cuda_core/tests/test_device.py", set(), CORE), + ("cuda_pathfinder/tests/test_loader.py", set(), {("pathfinder", None)}), + ("cuda_python/tests/test_import.py", set(), META), + ("cuda_python_test_helpers/cuda_python_test_helpers/cuda_utils.py", set(), ALL), + ("benchmarks/cuda_bindings/run_pyperf.py", set(), ALL), + ], +) +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_source_and_test_changes_select_their_consumers(path, builds, tests): + plan = plan_for(path) + assert selected(plan, "needs_build") == builds + assert selected(plan, "needs_test") == tests + assert {platform for platform, enabled in plan["jobs"]["platforms"].items() if enabled} == {"linux", "windows"} + + +@pytest.mark.parametrize("package", PACKAGES, ids=lambda package: package.package_root) +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_bindings_changes_reuse_core_and_test_the_matching_major(package): + own = {("bindings", package.package_root), ("python", package.package_root)} + plan = plan_for(f"{package.package_root}/cuda/bindings/driver.pyx") + assert selected(plan, "needs_build") == own + assert selected(plan, "needs_test") == own | {("core", package.cuda_variant)} + assert plan["baseline"] == {"run_id": "123", "sha": "base"} + assert {root for root, enabled in plan["jobs"]["sdist_package_roots"].items() if enabled} == {package.package_root} + + tests_only = plan_for(f"{package.package_root}/tests/test_driver.py") + assert selected(tests_only, "needs_build") == set() + assert selected(tests_only, "needs_test") == {("bindings", package.package_root)} + + +@pytest.mark.parametrize("path", ["ci/tools/env-vars", ".github/workflows/build-wheel.yml", "unknown-build-input"]) +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_shared_or_unknown_infrastructure_requires_every_component(path): + plan = plan_for(path) + assert selected(plan, "needs_build") == ALL + assert selected(plan, "needs_test") == ALL + assert plan["baseline"]["run_id"] == "" + + +@pytest.mark.parametrize("platform", ["linux", "windows"]) +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_platform_test_infrastructure_reuses_wheels_but_runs_every_suite(platform): + plan = plan_for(f".github/workflows/test-wheel-{platform}.yml") + assert selected(plan, "needs_build") == set() + assert selected(plan, "needs_test") == ALL + assert {name for name, enabled in plan["jobs"]["platforms"].items() if enabled} == {platform} + + +@pytest.mark.parametrize( + "path", + [ + "README.md", + "AGENTS.md", + "cuda_core/docs/source/install.rst", + "cuda_core/pixi.lock", + "cuda_core/pixi.toml", + ".github/workflows/runner-trust-probe.yml", + "ci/tools/runner_trust_probe.sh", + "ci/tools/runner_trust_probe.ps1", + "ci/tools/runner_trust_probe_node.js", + ], +) +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_documentation_and_environment_only_changes_do_not_rebuild_packages(path): + plan = plan_for(path) + assert selected(plan, "needs_build") == set() + assert selected(plan, "needs_test") == set() + + +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_missing_baselines_and_non_bindings_tags_require_full_coverage(): + cases = [plan_for(baseline=False), plan_for(release_tag="cuda-core-v1.3.0")] + cases.extend(plan_for(f"{package.package_root}/cuda/bindings/driver.pyx", baseline=False) for package in PACKAGES) + for plan in cases: + assert selected(plan, "needs_build") == ALL + assert selected(plan, "needs_test") == ALL + assert plan["baseline"] == {"run_id": "", "sha": ""} + + +@pytest.mark.parametrize("package", PACKAGES, ids=lambda package: package.package_root) +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_release_tags_build_only_their_own_bindings_and_metapackage(package): + plan = plan_for("ci/tools/env-vars", release_tag=f"v{package.ctk_target}.99") + expected = {("bindings", package.package_root), ("python", package.package_root)} + assert selected(plan, "needs_build") == expected + assert selected(plan, "needs_test") == expected + assert plan["sources"]["pathfinder"] == "published" + assert plan["baseline"]["run_id"] == "" + assert {root for root, enabled in plan["jobs"]["sdist_package_roots"].items() if enabled} == {package.package_root} + + +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_mixed_changes_combine_coverage_without_rebuilding_unchanged_core(): + package = PACKAGES[0] + plan = plan_for(f"{package.package_root}/cuda/bindings/driver.pyx", "cuda_core/tests/test_device.py") + own = {("bindings", package.package_root), ("python", package.package_root)} + assert selected(plan, "needs_build") == own + assert selected(plan, "needs_test") == own | CORE + + both = plan_for(*(f"{package.package_root}/cuda/bindings/driver.pyx" for package in PACKAGES)) + assert selected(both, "needs_build") == BINDINGS | META + assert selected(both, "needs_test") == BINDINGS | CORE | META + + +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_symlinked_package_inputs_are_not_mistaken_for_documentation_only(tmp_path: Path): + (tmp_path / "cuda_python").mkdir() + (tmp_path / "README.md").write_text("readme", encoding="utf-8") + (tmp_path / "cuda_python" / "README.md").symlink_to("../README.md") + paths = _expand_linked_paths(["README.md"], ["cuda_python/README.md"], root=tmp_path) + for changes in (paths, ["cuda_python/README.md"]): + plan = plan_for(*changes, linked_paths={"cuda_python/README.md"}) + assert selected(plan, "needs_build") == BINDINGS | META + assert selected(plan, "needs_test") == META + + +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_registered_source_roots_drive_bindings_selection(): + packages = tuple(replace(package, package_root=f"renamed_{index}") for index, package in enumerate(PACKAGES)) + config = replace(CONFIG, package_roots=packages) + package = packages[0] + plan = plan_for(f"{package.package_root}/cuda/bindings/driver.pyx", config=config) + own = {("bindings", package.package_root), ("python", package.package_root)} + assert selected(plan, "needs_build") == own + assert selected(plan, "needs_test") == own | {("core", package.cuda_variant)} diff --git a/ci/tools/tests/test_merge_cuda_core_wheels.py b/ci/tools/tests/test_merge_cuda_core_wheels.py new file mode 100644 index 00000000000..b1eab55e291 --- /dev/null +++ b/ci/tools/tests/test_merge_cuda_core_wheels.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from zipfile import ZipFile + +import pytest +from wheel.wheelfile import WheelFile + +from ci.tools.merge_cuda_core_wheels import merge_wheels + + +@pytest.mark.agent_authored(model="gpt-6-astra") +def test_merged_wheel_preserves_floor_helper_and_all_abi_variants(tmp_path): + wheels = [] + floor_helper = "SUPPORTED_BINDINGS = {12: '12.9.8', 13: '13.4.1'}\n" + for major in (12, 13): + path = tmp_path / f"cuda_core-1.2.3-py3-none-any.cu{major}.whl" + with WheelFile(path, "w") as wheel: + wheel.writestr("cuda/core/__init__.py", "from . import _bindings_floor\n") + wheel.writestr("cuda/core/_version.py", "__version__ = '1.2.3'\n") + wheel.writestr("cuda/core/_bindings_floor.py", floor_helper) + wheel.writestr("cuda/core/_abi.py", f"CUDA_MAJOR = {major}\n") + wheel.writestr( + "cuda_core-1.2.3.dist-info/METADATA", "Metadata-Version: 2.1\nName: cuda-core\nVersion: 1.2.3\n" + ) + wheel.writestr( + "cuda_core-1.2.3.dist-info/WHEEL", + "Wheel-Version: 1.0\nGenerator: test\nRoot-Is-Purelib: true\nTag: py3-none-any\n", + ) + wheels.append(path) + + merged = merge_wheels(wheels, tmp_path / "merged", show_wheel_contents=False) + + with ZipFile(merged) as wheel: + assert wheel.read("cuda/core/_bindings_floor.py").decode() == floor_helper + for major in (12, 13): + assert wheel.read(f"cuda/core/cu{major}/_abi.py").decode() == f"CUDA_MAJOR = {major}\n" + assert "cuda/core/_abi.py" not in wheel.namelist() diff --git a/ci/tools/tests/test_select_test_matrix.py b/ci/tools/tests/test_select_test_matrix.py new file mode 100644 index 00000000000..7e913fa8a0d --- /dev/null +++ b/ci/tools/tests/test_select_test_matrix.py @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from ci.tools.select_test_matrix import select_test_matrix + +PACKAGES = [ + { + "package_root": "bindings12", + "toolkit_version": "12.9.1", + "cuda_variant": "cu12", + "release_status": "maintenance", + }, + {"package_root": "bindings13", "toolkit_version": "13.4.2", "cuda_variant": "cu13", "release_status": "current"}, +] + + +def rows(*toolkits): + return [{"CUDA_VER": toolkit, "DRIVER": "latest"} for toolkit in toolkits] + + +def plan(*, bindings=False, core=True, meta=False): + return { + "jobs": {"test_cuda_majors": {"cu12": True, "cu13": False}}, + "sources": {"pathfinder": "artifact"}, + "modules": { + "pathfinder": {"needs_test": False}, + "bindings": {"package_roots": {package["package_root"]: {"needs_test": bindings} for package in PACKAGES}}, + "core": {"cuda_majors": {"cu12": {"needs_test": core}, "cu13": {"needs_test": core}}}, + "python": {"package_roots": {package["package_root"]: {"needs_test": meta} for package in PACKAGES}}, + }, + } + + +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_exact_toolkit_rows_use_local_bindings_and_older_toolkits_use_published_floors(): + matrix = select_test_matrix(rows("12.9.1", "12.6.3", "13.4.2", "13.0.2"), PACKAGES) + + assert [(row["CUDA_VARIANT"], row["BINDINGS_SOURCE"], row["BINDINGS_SOURCE_DIR"]) for row in matrix["include"]] == [ + ("cu12", "local", "bindings12"), + ("cu12", "floor", ""), + ("cu13", "local", "bindings13"), + ("cu13", "floor", ""), + ] + + +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_workplan_retains_only_the_required_cuda_major_and_suites(): + matrix = select_test_matrix( + rows("12.9.1", "12.6.3", "13.4.2"), PACKAGES, workplan=plan(bindings=True, core=False, meta=True) + ) + + # Without Core or Pathfinder tests the older-toolkit row has no applicable + # suites. The required maintenance bindings/metapackage row must remain. + assert [(row["CUDA_VER"], row["BINDINGS_SOURCE_DIR"]) for row in matrix["include"]] == [("12.9.1", "bindings12")] + + +@pytest.mark.parametrize("component", ("bindings", "meta")) +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_filter_cannot_silently_drop_required_local_wheel_coverage(component): + with pytest.raises(ValueError, match="omits required local-wheel coverage: bindings12"): + select_test_matrix(rows("12.6.3", "13.4.2"), PACKAGES, workplan=plan(**{component: True})) + + +@pytest.mark.parametrize("test_mode", ("nightly-pytorch", "standard")) +@pytest.mark.agent_authored(model="gpt-6.1-sol") +def test_nightly_subsets_may_use_published_floor_bindings(test_mode): + matrix = select_test_matrix(rows("12.6.3"), PACKAGES, build_type="nightly", test_mode=test_mode) + + assert [row["BINDINGS_SOURCE"] for row in matrix["include"]] == ["floor"] From 718fd32488711fd8433be23c07935e2bae489b1a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 1 Nov 2026 19:31:18 -0800 Subject: [PATCH 2/2] ci: test both registered source environments nightly [skip ci] Signed-off-by: Ralf W. Grosse-Kunstleve --- .github/workflows/ci-pixi-source-test.yml | 53 +++++++++++++++-------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci-pixi-source-test.yml b/.github/workflows/ci-pixi-source-test.yml index 32caef7520c..05084e90e53 100644 --- a/.github/workflows/ci-pixi-source-test.yml +++ b/.github/workflows/ci-pixi-source-test.yml @@ -265,6 +265,8 @@ jobs: if: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.repository_owner == 'nvidia' }} runs-on: ubuntu-latest timeout-minutes: 60 + outputs: + bindings-packages: ${{ steps.roundtrip.outputs.bindings-packages }} steps: - name: Checkout ${{ github.event.repository.name }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -289,6 +291,10 @@ jobs: uses: ./.github/actions/setup-pixi - name: Build current, another ABI, then current again in one checkout + id: roundtrip + env: + EVENT_NAME: ${{ github.event_name }} + REQUESTED_CUDA_ENV: ${{ inputs.cuda-env || '' }} run: | current_package=$(python3 -m ci.tools.bindings_config --release-status current) bindings_packages=$(python3 -m ci.tools.bindings_config --package-roots) @@ -297,6 +303,22 @@ jobs: '[.[] | select(.cuda_variant != $current_env) | .cuda_variant] | first // empty' \ <<< "${bindings_packages}") + # Scheduled source tests cover both registered ABIs; manual runs + # retain a single selected ABI, defaulting to the current package. + if [[ "${EVENT_NAME}" == schedule ]]; then + selected_packages="${bindings_packages}" + else + selected_env="${REQUESTED_CUDA_ENV:-${current_env}}" + selected_packages=$(jq -c --arg cuda_env "${selected_env}" \ + '[.[] | select(.cuda_variant == $cuda_env)]' <<< "${bindings_packages}") + if [[ "${selected_packages}" == '[]' ]]; then + registered_envs=$(jq -r '[.[].cuda_variant] | join(", ")' <<< "${bindings_packages}") + echo "::error::cuda-env must be a registered pixi environment: ${registered_envs}" + exit 1 + fi + fi + echo "bindings-packages=${selected_packages}" >> "$GITHUB_OUTPUT" + for cuda_env in "${current_env}" "${other_env}" "${current_env}"; do echo "::group::${cuda_env}" pixi run --manifest-path cuda_core -e "${cuda_env}" \ @@ -328,8 +350,13 @@ jobs: # ── Nightly: full `pixi run test` on a GPU runner ── full-test: - name: "pixi run test (${{ inputs.cuda-env || 'current' }}, linux-64, GPU)" + name: "pixi run test (${{ matrix.bindings-package.cuda_variant }}, linux-64, GPU)" if: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.repository_owner == 'nvidia' }} + needs: build-identity-roundtrip + strategy: + fail-fast: false + matrix: + bindings-package: ${{ fromJSON(needs.build-identity-roundtrip.outputs.bindings-packages) }} runs-on: "linux-amd64-gpu-l4-latest-1" # same label scheme as test-wheel-linux.yml timeout-minutes: 90 container: @@ -374,27 +401,15 @@ jobs: - name: Prepare selected CUDA environment env: - REQUESTED_CUDA_ENV: ${{ inputs.cuda-env || '' }} + CUDA_ENV: ${{ matrix.bindings-package.cuda_variant }} + BINDINGS_ROOT: ${{ matrix.bindings-package.package_root }} + RELEASE_STATUS: ${{ matrix.bindings-package.release_status }} run: | - if [[ -n "${REQUESTED_CUDA_ENV}" ]]; then - bindings_packages=$(python3 -m ci.tools.bindings_config --package-roots) - bindings_package=$(jq -c --arg cuda_env "${REQUESTED_CUDA_ENV}" \ - '.[] | select(.cuda_variant == $cuda_env)' <<< "${bindings_packages}") - if [[ -z "${bindings_package}" ]]; then - registered_envs=$(jq -r '[.[].cuda_variant] | join(", ")' <<< "${bindings_packages}") - echo "::error::cuda-env must be a registered pixi environment: ${registered_envs}" - exit 1 - fi - else - bindings_package=$(python3 -m ci.tools.bindings_config --release-status current) - fi - cuda_env=$(jq -r '.cuda_variant' <<< "${bindings_package}") - if [[ $(jq -r '.release_status' <<< "${bindings_package}") == maintenance ]]; then - bindings_root=$(jq -r '.package_root' <<< "${bindings_package}") + if [[ "${RELEASE_STATUS}" == maintenance ]]; then # Match build-smoke's version preparation before any pixi source build. - python3 -m ci.tools.bindings_scm_version --package-root "${bindings_root}" --sha "$GITHUB_SHA" + python3 -m ci.tools.bindings_scm_version --package-root "${BINDINGS_ROOT}" --sha "$GITHUB_SHA" fi - echo "CUDA_ENV=${cuda_env}" | tee -a "$GITHUB_ENV" + echo "CUDA_ENV=${CUDA_ENV}" | tee -a "$GITHUB_ENV" - name: Setup proxy cache uses: nv-gha-runners/setup-proxy-cache@main