Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:

- name: Run CI tools unit tests
run: |
python -m pip install pytest pyyaml==6.0.3 setuptools-scm
python -m pip install -e './ci[test]'
# Standalone CI tool tests; skip repo-root conftest.py (imports cuda.pathfinder).
python -m pytest -v --noconftest ci/tools/tests

Expand Down
89 changes: 72 additions & 17 deletions .github/workflows/ci-pixi-source-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
# 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 bindings + core, imports them,
# builds the cython test extensions and checks placement. Catches the
# - build-smoke (PRs): CPU-only. Source-builds both bindings roots and current
# Core, imports them, builds Cython 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
Expand Down Expand Up @@ -42,19 +42,26 @@ on:
# 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_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_core/tests/cython/**"
- "ci/versions.yml"
- "ci/pyproject.toml"
- "ci/tools/bindings_config.py"
- "ci/tools/bindings_scm_version.py"
- "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: string
type: choice
options: ["cu13", "cu12"]
default: "cu13"

defaults:
Expand All @@ -73,12 +80,19 @@ env:
jobs:
# ── PR guard: CPU-only build + import + placement smoke ──
build-smoke:
name: "build smoke (cu13, linux-64, CPU)"
name: "build smoke (${{ matrix.cuda-env }}, 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
Expand All @@ -91,28 +105,46 @@ jobs:
# 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' }}
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: Setup pixi
uses: ./.github/actions/setup-pixi

- name: Source-build + import + cython-placement smoke
env:
CUDA_ENV: ${{ inputs.cuda-env || 'cu13' }}
run: |
# pathfinder: pure-Python, no GPU.
pixi run -e "${CUDA_ENV}" test-pathfinder

# bindings + core: force the source build (catches nvrtc/driver
# Force each bindings source build (catches nvrtc/driver
# compile errors like #2182) and import them (catches ABI mismatches).
pixi run --manifest-path cuda_bindings_13 -e "${CUDA_ENV}" \
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')"
if [[ "${CUDA_ENV}" == cu13 ]]; then
pixi run --manifest-path cuda_core -e "${CUDA_ENV}" \
python -c "import cuda.core; print('core import OK')"
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 cuda_bindings_13 -e "${CUDA_ENV}" build-cython-tests
pixi run --manifest-path cuda_core -e "${CUDA_ENV}" build-cython-tests
for d in cuda_bindings_13/tests/cython cuda_core/tests/cython; do
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)
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
Expand All @@ -131,8 +163,7 @@ jobs:
# The round trip (not just cu13 -> cu12) is what catches the second half:
# coming back to cu13 must not silently reuse the cu12 extension.
#
# cuda_core only: cuda-bindings cannot be source-built in its cu12
# environment at all, for reasons unrelated to stale artifacts.
# Core only: bindings majors have separate source roots and environments.
build-identity-roundtrip:
name: "cu13 -> cu12 -> cu13 round trip (linux-64, CPU)"
if: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.repository_owner == 'nvidia' }}
Expand Down Expand Up @@ -186,10 +217,14 @@ jobs:

# ── Nightly: full `pixi run test` on a GPU runner ──
full-test:
name: "pixi run test (${{ inputs.cuda-env || 'cu13' }}, linux-64, GPU)"
name: "pixi run test (${{ matrix.cuda-env }}, 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
Expand Down Expand Up @@ -218,12 +253,32 @@ jobs:
# third_party/cccl provides the CCCL headers cuda.core compiles against.
submodules: true

- name: Trust the checkout for container source builds
# 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' }}
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: Setup pixi
uses: ./.github/actions/setup-pixi

- name: pixi run test
run: pixi run -e "${{ inputs.cuda-env || 'cu13' }}" test
env:
CUDA_ENV: ${{ matrix.cuda-env }}
run: pixi run -e "${CUDA_ENV}" test
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ jobs:

- name: Test CI tools
run: |
python -m pip install pytest==9.1.0 pyyaml==6.0.3 setuptools-scm
python -m pip install -e './ci[test]'
python -m pytest -v --noconftest ci/tools/tests

- name: Compute CI workplan
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ repos:
entry: python -m ci.tools.check_pixi_cuda_version
language: python
additional_dependencies: [pyyaml==6.0.3]
files: '^(ci/versions\.yml|ci/tools/(bindings_config|check_pixi_cuda_version)\.py|cuda_bindings_13/pixi\.toml|cuda_core/pixi\.toml)$'
files: '^(ci/versions\.yml|ci/tools/(bindings_config|check_pixi_cuda_version)\.py|cuda_bindings_(?:12|13)/pixi\.toml|cuda_core/pixi\.toml)$'
pass_filenames: false

- id: check-mempool-hygiene
Expand Down Expand Up @@ -162,7 +162,7 @@ repos:
rev: "3e8a8703264a2f4a69428a0aa4dcb512790b2c8c" # frozen: v6.0.0
hooks:
- id: check-added-large-files
exclude: '^(?:cuda_bindings_13/cuda/bindings/(?:nvml\.pyx|_v2/driver\.pyx)|cuda_bindings_12/cuda/bindings/(?:driver\.pyx|runtime\.pyx\.in|nvml\.pyx))$'
exclude: '^(?:cuda_bindings_13/cuda/bindings/(?:nvml\.pyx|_v2/driver\.pyx)|cuda_bindings_12/cuda/bindings/(?:driver\.pyx|runtime\.pyx\.in|nvml\.pyx)|cuda_bindings_(?:12|13)/pixi\.lock)$'
- id: check-case-conflict
- id: check-docstring-first
- id: check-merge-conflict
Expand Down
39 changes: 38 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,33 @@ guide for package-specific conventions and workflows.
## Package map

- `cuda_pathfinder/`: Pure-Python library discovery and loading utilities.
- `cuda_bindings_13/`: Low-level CUDA host API bindings (Cython-heavy).
- `cuda_bindings_12/`: CUDA 12.9 maintenance bindings (Cython-heavy).
- `cuda_bindings_13/`: Current CUDA 13 bindings (Cython-heavy).
- `cuda_core/`: High-level Pythonic CUDA APIs built on top of bindings.
- `cuda_python/`: Metapackage and docs aggregation.

## Changes across bindings majors

Both `cuda_bindings_12/` and `cuda_bindings_13/` are maintained on `main`.
When implementing or reviewing a change in either root, inspect the other
root's corresponding implementation, tests, and relevant build, packaging,
or documentation files, including files absent from the diff. Check shared
instructions and configuration affected by the change too. Apply this check
in both directions, and when changing shared build helpers, generation
inputs, CI selection, or release routing.

- Make matching changes where the affected behavior applies to both roots.
Preserve intentional differences in supported APIs, toolkit inputs, source
layout, and compatibility behavior. Do not copy changes mechanically or add an unsupported API to make the roots match.
- Follow each package's generation instructions: change the generation source
and regenerate each affected root against its own inputs. Do not hand-edit
generated output.
- In the implementation or review summary, name the counterpart files inspected
and identify the matching changes or explain concretely why none are needed.
Reviewers must verify that assessment and flag omitted applicable changes;
absence from the diff is not evidence that the other root is unaffected.
- Preserve byte-for-byte invariants for package-local copies of shared helpers.

# Git-derived version lifecycle

Package versions are derived from reachable Git tags by `setuptools-scm`.
Expand All @@ -36,6 +59,20 @@ For each package tag namespace and target release `X.Y.Z`:
existing history.
- Treat pushed tags as immutable. Correct an unsuitable tagged state with a
new versioned milestone rather than moving or replacing the existing tag.
- Reserve a new preview target from the next globally unused release number,
checking canonical tags and coordinating releases on every maintenance or
stabilization branch. A release on another branch consumes that number even
when its tag is not reachable from `main`. Update `scm-bootstrap-release`
before another build if such a release consumes its target; do not bump the
field per PR or use it to override suitable reachable tags.
- Moving a package's SCM selector to a new toolkit minor also needs a matching
reachable milestone. Updating the selector alone can produce an incorrect
placeholder version. Validate previews with local tags in an independent
disposable clone until the release history supplies the right milestone.
- For a newly integrated maintenance root, create its first retained internal
milestone only after the integration group has landed and been validated on
`main`. Retained tag creation requires an explicit maintainer decision;
preparation of a PR stack does not authorize creating or pushing it.

Use the package's established tag namespace, such as `vX.Y.Z...` for
`cuda-bindings` and `cuda-python`, `cuda-core-vX.Y.Z...` for `cuda-core`, and
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ The list of available interfaces is:
* cuFile
* NVML

## Bindings source roots

Current CUDA 13 bindings live in [`cuda_bindings_13/`](cuda_bindings_13/), and
CUDA 12.9 sources have a separate
[developer environment](cuda_bindings_12/README.md). Both roots build the same
`cuda-bindings` distribution and `cuda.bindings` namespace; develop and test
them in separate environments.

## License

CUDA Python is licensed under the [Apache License 2.0](./LICENSE). Third-party
Expand All @@ -59,7 +67,8 @@ The root `LICENSE` governs the repository as a whole:

| Subproject | License | License file |
| ---------------- | ---------- | -------------------------------------------------------- |
| `cuda.bindings` | Apache-2.0 | [`cuda_bindings_13/LICENSE`](./cuda_bindings_13/LICENSE) |
| `cuda.bindings` (CUDA 13) | Apache-2.0 | [`cuda_bindings_13/LICENSE`](./cuda_bindings_13/LICENSE) |
| `cuda.bindings` (CUDA 12) | Apache-2.0 | [`cuda_bindings_12/LICENSE`](./cuda_bindings_12/LICENSE) |
| `cuda.core` | Apache-2.0 | [`cuda_core/LICENSE`](./cuda_core/LICENSE) |
| `cuda.pathfinder`| Apache-2.0 | [`cuda_pathfinder/LICENSE`](./cuda_pathfinder/LICENSE) |
| `cuda-python` | Apache-2.0 | [`cuda_python/LICENSE`](./cuda_python/LICENSE) |
26 changes: 26 additions & 0 deletions ci/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[build-system]
requires = ["setuptools>=77.0.0"]
build-backend = "setuptools.build_meta"

[project]
name = "cuda-python-ci-tools"
version = "0.0.0"
description = "Private CI support for the CUDA Python repository"
authors = [{ name = "NVIDIA Corporation" }]
license = "Apache-2.0"
requires-python = ">=3.11"
dependencies = [
"packaging==26.0",
"PyYAML==6.0.3",
"setuptools-scm>=8,!=10.1",
]

[project.optional-dependencies]
test = ["pytest==9.1.0", "wheel"]

[tool.setuptools]
packages = ["ci", "ci.tools"]
package-dir = { ci = "." }
45 changes: 44 additions & 1 deletion ci/tools/bindings_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

from __future__ import annotations

import argparse
import json
import sys
from dataclasses import dataclass
from pathlib import Path

Expand Down Expand Up @@ -35,10 +38,21 @@ def cuda_major(self) -> str:
def cuda_variant(self) -> str:
return f"cu{self.cuda_major}"

def to_dict(self) -> dict[str, str]:
return {
"package_root": self.package_root,
"toolkit_version": self.toolkit_version,
"release_status": self.release_status,
"ctk_target": self.ctk_target,
"cuda_major": self.cuda_major,
"cuda_variant": self.cuda_variant,
}


@dataclass(frozen=True)
class BindingsConfig:
package_roots: tuple[BindingsPackage, ...]
python_supported: tuple[str, ...]

def get_package(self, package_root: str) -> BindingsPackage:
for package in self.package_roots:
Expand All @@ -57,4 +71,33 @@ def load_config(path: Path = DEFAULT_CONFIG) -> BindingsConfig:
for field, release_status in (("build", "current"), ("prev_build", "maintenance")):
major, minor, patch = map(int, raw["cuda"][field]["version"].split("."))
packages.append(BindingsPackage(f"cuda_bindings_{major}", (major, minor, patch), release_status))
return BindingsConfig(tuple(packages))
return BindingsConfig(tuple(packages), tuple(raw["python"]["supported"]))


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--config", type=Path, default=DEFAULT_CONFIG)
output = parser.add_mutually_exclusive_group()
output.add_argument("--package-roots", action="store_true")
output.add_argument("--release-status", choices=("current", "maintenance"))
args = parser.parse_args(argv)

config = load_config(args.config)
if args.release_status:
result = config.package_for_release_status(args.release_status).to_dict()
else:
packages = [package.to_dict() for package in config.package_roots]
result = (
packages
if args.package_roots
else {
"python": {"supported": list(config.python_supported)},
"package_roots": packages,
}
)
print(json.dumps(result, separators=(",", ":"), sort_keys=True))
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading