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
4 changes: 2 additions & 2 deletions .github/workflows/test-wheel-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -248,14 +248,14 @@ jobs:
fi

- name: Display structure of downloaded cuda-python artifacts
if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'published' }}
if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'floor' }}
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 != 'published' }}
env.BINDINGS_SOURCE != 'floor' }}
run: |
pwd
ls -lahR $CUDA_BINDINGS_ARTIFACTS_DIR
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-wheel-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,14 @@ jobs:
fi

- name: Display structure of downloaded cuda-python artifacts
if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'published' }}
if: ${{ env.TEST_PYTHON == 'true' && env.BINDINGS_SOURCE != 'floor' }}
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 != 'published' }}
env.BINDINGS_SOURCE != 'floor' }}
run: |
Get-Location
Get-ChildItem -Recurse -Force $env:CUDA_BINDINGS_ARTIFACTS_DIR | Select-Object Mode, LastWriteTime, Length, FullName
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ cuda_bindings/cuda/bindings/utils/_get_handle.pyx

# Version files from setuptools_scm
_version.py
# Generated by cuda_core/build_hooks.py at build time (see cuda/core/__init__.py).
cuda_core/cuda/core/_build_info.py

# Distribution / packaging
.Python
Expand Down
74 changes: 74 additions & 0 deletions ci/tools/cuda_core_bindings_floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

"""Print the cuda-bindings floor of a cuda-core wheel for one CUDA major.

cuda_core_bindings_floor.py --wheel dist/cuda_core-*.whl --major 13
-> 13.4.1

CI installs `cuda-bindings==<floor>` next to a freshly built cuda-core wheel to
test the oldest cuda-bindings that wheel supports (BINDINGS_SOURCE=floor in
ci/tools/env-vars). The floor is read from the wheel under test rather than
from the checkout, so a nightly job that tests a wheel built from another
commit reads that wheel's floor.

The wheel carries the import-free module cuda/core/_bindings_floor.py, at top
level in a single-major build and under cuda/core/cu<major>/ in the merged
wheel; this script reads the CUDA_BINDINGS_FLOOR literal out of it (without
running the module) and prints the entry for `major` as a dotted version.
"""

from __future__ import annotations

import argparse
import ast
import sys
import zipfile
from pathlib import Path

MODULE = "_bindings_floor.py"


def floors_from_source(source: str) -> dict[int, tuple[int, int, int]]:
"""The CUDA_BINDINGS_FLOOR literal of _bindings_floor.py, parsed without executing it."""
for node in ast.parse(source, MODULE).body:
if isinstance(node, ast.AnnAssign):
targets = [node.target]
elif isinstance(node, ast.Assign):
targets = node.targets
else:
continue
if node.value is not None and any(isinstance(t, ast.Name) and t.id == "CUDA_BINDINGS_FLOOR" for t in targets):
return ast.literal_eval(node.value)
raise SystemExit(f"{MODULE} does not assign CUDA_BINDINGS_FLOOR")


def floor_from_source(source: str, major: int) -> str:
floors = floors_from_source(source)
if major not in floors:
raise SystemExit(f"CUDA {major} is not a supported major (floors: {sorted(floors)})")
return ".".join(str(part) for part in floors[major])


def floor_from_wheel(wheel: Path, major: int) -> str:
with zipfile.ZipFile(wheel) as zf:
names = set(zf.namelist())
for candidate in (f"cuda/core/cu{major}/{MODULE}", f"cuda/core/{MODULE}"):
if candidate in names:
return floor_from_source(zf.read(candidate).decode("utf-8"), major)
raise SystemExit(f"{wheel.name} contains no {MODULE}; is it a cuda-core wheel?")


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--wheel", type=Path, required=True, help="the cuda-core wheel under test")
parser.add_argument("--major", type=int, required=True, help="CUDA major series (12 or 13)")
args = parser.parse_args(argv)
print(floor_from_wheel(args.wheel, args.major))
return 0


if __name__ == "__main__":
sys.exit(main())
29 changes: 23 additions & 6 deletions ci/tools/env-vars
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,40 @@ elif [[ "${1}" == "test" ]]; then
# 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
# published — install from PyPI (cuda-bindings==${TEST_CUDA_MAJOR}.${TEST_CUDA_MINOR}.*)
# floor — install the oldest cuda-bindings the cuda-core wheel under test
# supports, from PyPI (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 one the wheel
# was built against, so those rows exercise new cuda-core + floor
# bindings + older CTK libraries, the skew 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})"
# The prior-major half of the cuda-core wheel is built against ci/versions.yml's
# prev_build toolkit (and the backport branch's bindings).
BUILD_PREV_CUDA_VER="$(sed -n '/prev_build:/,/version:/s/.*version: *"\([^"]*\)".*/\1/p' ci/versions.yml)"
BUILD_PREV_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_PREV_CUDA_VER})"

if [[ ${BUILD_CUDA_MAJOR} != ${TEST_CUDA_MAJOR} ]]; then
# Major mismatch (e.g. build=13.x, test=12.x): use the backport branch.
BINDINGS_SOURCE=backport
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_SOURCE=floor
else
# Prior major, same minor (e.g. build=13.x, test=12.9): the backport branch.
BINDINGS_SOURCE=backport
fi
elif [[ ${BUILD_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then
# Same major, minor mismatch (e.g. build=13.2, test=13.0): use published
# bindings from PyPI to test the real-world backward-compat scenario.
BINDINGS_SOURCE=published
# 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
else
Expand Down
10 changes: 7 additions & 3 deletions ci/tools/run-tests
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,14 @@ elif [[ "${test_module}" == "core" || "${test_module}" == nightly-* ]]; then

# Resolve bindings based on BINDINGS_SOURCE (set by env-vars):
# main/backport → local wheel from artifacts dir
# published → install from PyPI by version
# floor → the oldest cuda-bindings the core wheel under test supports,
# read from that wheel, installed from PyPI
BINDINGS_ARGS=()
if [[ "${BINDINGS_SOURCE}" == "published" ]]; then
BINDINGS_ARGS+=("cuda-bindings==${TEST_CUDA_MAJOR}.${TEST_CUDA_MINOR}.*")
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}")"
echo "cuda-bindings floor of ${CORE_WHL_FOR_FLOOR[0]##*/} for CUDA ${TEST_CUDA_MAJOR}: ${BINDINGS_FLOOR}"
BINDINGS_ARGS+=("cuda-bindings==${BINDINGS_FLOOR}")
else
BINDINGS_ARGS=("${CUDA_BINDINGS_ARTIFACTS_DIR}"/*.whl)
if [[ "${LOCAL_CTK}" != 1 ]]; then
Expand Down
70 changes: 70 additions & 0 deletions ci/tools/tests/test_cuda_core_bindings_floor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

import importlib.util
import zipfile
from pathlib import Path

import pytest

TOOLS = Path(__file__).resolve().parent.parent
REPO = TOOLS.parent.parent
FLOOR_MODULE = REPO / "cuda_core" / "cuda" / "core" / "_bindings_floor.py"


def _load(name, path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


tool = _load("cuda_core_bindings_floor", TOOLS / "cuda_core_bindings_floor.py")
floor_module = _load("cuda_core_bindings_floor_module", FLOOR_MODULE)


def _expected(major):
return floor_module.format_version(floor_module.CUDA_BINDINGS_FLOOR[major])


def _wheel(tmp_path, entries):
path = tmp_path / "cuda_core-1.3.0-cp312-cp312-linux_x86_64.whl"
with zipfile.ZipFile(path, "w") as zf:
for name in entries:
zf.writestr(name, FLOOR_MODULE.read_text(encoding="utf-8"))
return path


@pytest.mark.agent_authored(model="claude-fable-5-1")
@pytest.mark.parametrize("major", [12, 13])
def test_reads_the_merged_wheel_layout(tmp_path, major):
wheel = _wheel(tmp_path, ["cuda/core/cu12/_bindings_floor.py", "cuda/core/cu13/_bindings_floor.py"])
assert tool.floor_from_wheel(wheel, major) == _expected(major)


@pytest.mark.agent_authored(model="claude-fable-5-1")
def test_reads_a_single_major_wheel(tmp_path):
wheel = _wheel(tmp_path, ["cuda/core/_bindings_floor.py"])
assert tool.floor_from_wheel(wheel, 13) == _expected(13)


@pytest.mark.agent_authored(model="claude-fable-5-1")
def test_rejects_a_wheel_without_the_module(tmp_path):
wheel = _wheel(tmp_path, [])
with pytest.raises(SystemExit, match="contains no _bindings_floor.py"):
tool.floor_from_wheel(wheel, 13)


@pytest.mark.agent_authored(model="claude-fable-5-1")
def test_rejects_an_unsupported_major(tmp_path):
wheel = _wheel(tmp_path, ["cuda/core/_bindings_floor.py"])
with pytest.raises(SystemExit, match="CUDA 11 is not a supported major"):
tool.floor_from_wheel(wheel, 11)


@pytest.mark.agent_authored(model="claude-fable-5-1")
def test_cli_prints_the_floor(tmp_path, capsys):
wheel = _wheel(tmp_path, ["cuda/core/_bindings_floor.py"])
assert tool.main(["--wheel", str(wheel), "--major", "13"]) == 0
assert capsys.readouterr().out.strip() == _expected(13)
18 changes: 14 additions & 4 deletions cuda_core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,20 @@ and agents should flag violations.
objects that are not meant to be shared (e.g., the thread-local `Device`) do not
need such guards (see #2321). Reference-count integrity is guaranteed; cache
value-identity/idempotency is not.
- **Entry points assume the GIL is held**: the helpers in `_cpp/rt/`
are called from Cython with the GIL held and do not re-acquire it. Driver and
destructor callbacks run at arbitrary times, so they take the GIL (`with gil`)
and probe for interpreter shutdown before touching Python objects.
- **Entry points work with or without the GIL**: the helpers in `_cpp/rt/`
are called from Cython both inside and outside `with nogil` blocks. They
never require the GIL, release it around driver calls, and never acquire it
while holding a C++ lock; the only paths that acquire it are the reporting
wrappers (`pw_*`, `report_*`) and the one-time driver function-table fill
(`ensure_fn_table()`, see `_cpp/rt/DESIGN.md`). Driver and destructor
callbacks run at arbitrary times, so they take the GIL (`with gil`) and probe
for interpreter shutdown before touching Python objects.
- **Driver calls go through the table**: C++ calls the driver with
`DRIVER_CALL(name, args...)`, whose pointers come from cuda-bindings'
resolved table, never from the Cython wrappers. A function the installed
driver may lack is gated in Cython on `cy_driver_version()` at the version
cuda-bindings requests it at (the number in `driver_api.hpp`); the C++
never checks a pointer for null.
- **Lock ordering -- release the GIL before entering the driver**: any CUDA work
reachable from a host callback or a retained object's `__del__` must release the
GIL before calling the driver, to avoid GIL/driver-lock deadlocks (see the
Expand Down
Loading
Loading