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
43 changes: 31 additions & 12 deletions ci/tools/cuda_core_bindings_floor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
from the checkout, so a nightly job that tests a wheel built from another
commit reads that wheel's floor.

The floors are declared once, by the `cu12`/`cu13` extras in
cuda_core/pyproject.toml, and the wheel's METADATA carries them as
`Requires-Dist: cuda-bindings[all]<14,>=13.4.1; extra == "cu13"`. This script
reads that line.
The floors are declared once, by the `minimal-sysctk12`/`minimal-sysctk13`
extras in cuda_core/pyproject.toml, which the `cu12`/`cu13` extras include. The
wheel's METADATA carries them as
`Requires-Dist: cuda-bindings<14,>=13.4.1; extra == "minimal-sysctk13"` and
`Requires-Dist: cuda-core[minimal-sysctk13]; extra == "cu13"`. This script
reads the floor of the `cu<major>` extra, following that self-reference.
"""

from __future__ import annotations
Expand All @@ -28,21 +30,38 @@
import zipfile
from pathlib import Path

# The cuda-bindings requirement of one `cu<major>` extra in METADATA. The name
# is followed by `[`, an operator or whitespace so that other names do not match.
# A cuda-bindings requirement of one extra in METADATA. The name is followed by
# `[`, an operator or whitespace so that other names do not match.
_REQUIRES_DIST_RE = re.compile(
r"^Requires-Dist:\s*cuda-bindings(?=[\[<>=!~\s])(?P<spec>[^;]*);\s*extra\s*==\s*['\"]cu(?P<major>\d+)['\"]"
r"^Requires-Dist:\s*cuda-bindings(?=[\[<>=!~\s])(?P<spec>[^;]*);\s*extra\s*==\s*['\"](?P<extra>[^'\"]+)['\"]"
)
# A self-reference of one extra, such as `cuda-core[minimal-sysctk13]; extra == "cu13"`.
_SELF_REFERENCE_RE = re.compile(
r"^Requires-Dist:\s*cuda-core\s*\[(?P<extras>[^\]]*)\]\s*;\s*extra\s*==\s*['\"](?P<extra>[^'\"]+)['\"]"
)
_FLOOR_RE = re.compile(r">=\s*(\d+)\.(\d+)\.(\d+)")


def _bindings_requirements(metadata: str, extra: str, seen: tuple[str, ...] = ()) -> list[tuple[str, str]]:
"""(line, specifier) of each versioned cuda-bindings requirement of `extra`, following self-references."""
seen = (*seen, extra)
found = []
for line in metadata.splitlines():
if (m := _REQUIRES_DIST_RE.match(line)) is not None and m.group("extra") == extra:
# An unversioned requirement such as `cuda-bindings[all]` only selects extras of cuda-bindings.
if re.sub(r"\[[^\]]*\]", "", m.group("spec")).strip():
found.append((line, m.group("spec")))
elif (m := _SELF_REFERENCE_RE.match(line)) is not None and m.group("extra") == extra:
for name in (name.strip() for name in m.group("extras").split(",")):
if name and name not in seen:
found.extend(_bindings_requirements(metadata, name, seen))
return found


def floor_from_metadata(metadata: str, major: int) -> str:
"""The floor that the `cu<major>` extra of a wheel's METADATA pins cuda-bindings to."""
for line in metadata.splitlines():
m = _REQUIRES_DIST_RE.match(line)
if m is None or int(m.group("major")) != major:
continue
floor = _FLOOR_RE.search(m.group("spec"))
for line, spec in _bindings_requirements(metadata, f"cu{major}"):
floor = _FLOOR_RE.search(spec)
if floor is None:
raise SystemExit(f"METADATA pins cuda-bindings without a floor for the cu{major} extra: {line.strip()!r}")
if int(floor.group(1)) != major:
Expand Down
35 changes: 35 additions & 0 deletions ci/tools/tests/test_cuda_core_bindings_floor.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,41 @@ def test_ignores_lookalike_names_and_other_extras():
assert tool.floor_from_metadata(metadata, 13) == "13.4.1"


# The layout in which cu<N> includes minimal-sysctk<N>, which pins the floor.
SELF_REFERENCING_METADATA = """\
Metadata-Version: 2.4
Name: cuda-core
Version: 1.3.1
Requires-Dist: cuda-pathfinder>=1.4.2
Requires-Dist: numpy
Provides-Extra: minimal-sysctk12
Requires-Dist: cuda-bindings<13,>=12.9.8; extra == "minimal-sysctk12"
Provides-Extra: minimal-sysctk13
Requires-Dist: cuda-bindings<14,>=13.5.0; extra == "minimal-sysctk13"
Provides-Extra: cu12
Requires-Dist: cuda-core[minimal-sysctk12]; extra == "cu12"
Requires-Dist: cuda-bindings[all]; extra == "cu12"
Requires-Dist: cuda-toolkit==12.*; extra == "cu12"
Provides-Extra: cu13
Requires-Dist: cuda-core[minimal-sysctk13]; extra == "cu13"
Requires-Dist: cuda-bindings[all]; extra == "cu13"
Requires-Dist: cuda-toolkit==13.*; extra == "cu13"
"""


@pytest.mark.agent_authored(model="claude-opus-5-5")
@pytest.mark.parametrize(("major", "floor"), [(12, "12.9.8"), (13, "13.5.0")])
def test_follows_the_self_reference_of_a_cu_extra(tmp_path, major, floor):
assert tool.floor_from_wheel(_wheel(tmp_path, SELF_REFERENCING_METADATA), major) == floor


@pytest.mark.agent_authored(model="claude-opus-5-5")
def test_rejects_a_self_reference_to_an_extra_without_cuda_bindings():
metadata = 'Requires-Dist: cuda-core[minimal-sysctk13]; extra == "cu13"\nRequires-Dist: cuda-bindings[all]; extra == "cu13"\n'
with pytest.raises(SystemExit, match="no cuda-bindings requirement for the cu13 extra"):
tool.floor_from_metadata(metadata, 13)


@pytest.mark.agent_authored(model="claude-fable-5-1")
def test_rejects_a_missing_extra(tmp_path):
with pytest.raises(SystemExit, match="no cuda-bindings requirement for the cu11 extra"):
Expand Down
11 changes: 7 additions & 4 deletions cuda_core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ Nothing else moves a floor. A new `cuda-bindings` release does not, and a
`cuda.core` release does not. Do not raise a floor to the newest release. To
bump:

1. Edit the `cuda-bindings` pin in the `cu12` or `cu13` extra in
`pyproject.toml`. That is the only version to type.
1. Edit the `cuda-bindings` pin in the `minimal-sysctk12` or
`minimal-sysctk13` extra in `pyproject.toml`. That is the only version to
type. The `cu12` and `cu13` extras include these extras and carry no
version of their own.
2. Check each `cuda.bindings.package_roots.<root>.toolkit_version` in
`ci/versions.yml`. Neither toolkit pin may sit below the corresponding
floor's major.minor, or CI builds a configuration that the build rejects.
Expand Down Expand Up @@ -120,8 +122,9 @@ the literal floor, until the floor is bumped. The order is:
is normally its first release.

A `cuda.core` release must not ship while the window is open. In the window,
the `cu<major>` extra can resolve to a `cuda-bindings` that fails the header
check at import, and CI does not test the literal floor.
the `minimal-sysctk<major>` and `cu<major>` extras can resolve to a
`cuda-bindings` that fails the header check at import, and CI does not test
the literal floor.

## Testing expectations

Expand Down
2 changes: 2 additions & 0 deletions cuda_core/DESCRIPTION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ Installation

pip install cuda-core[cu12]

Use ``[cu13]`` for CUDA 13. With a CUDA Toolkit installed on the system, ``[minimal-sysctk12]`` and ``[minimal-sysctk13]`` install only ``cuda-bindings``.

Please refer to the `installation instructions <https://nvidia.github.io/cuda-python/cuda-core/latest/install.html>`_ for different ways of installing ``cuda.core``, including building from source.
44 changes: 37 additions & 7 deletions cuda_core/cuda/core/_bindings_floor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@
https://github.com/NVIDIA/cuda-python/issues/2783 and the support policy in the
documentation.

Exactly one place declares the floors: the ``cu12`` and ``cu13`` extras in
``pyproject.toml``. Each extra pins ``cuda-bindings>=<floor>,<<next major>``.
Everything else derives from them through :func:`floors_from_extras`:
Exactly one place declares the floors: the ``minimal-sysctk12`` and
``minimal-sysctk13`` extras in ``pyproject.toml``. Each pins
``cuda-bindings>=<floor>,<<next major>``. The ``cu12`` and ``cu13`` extras
include them through a ``cuda-core[minimal-sysctk<major>]`` self-reference, so
that pip and uv see the floor whichever extra a dependent requests. Everything
else reads the floor of each ``cu<major>`` extra, following that
self-reference, through :func:`floors_from_extras`:

- The build backend, ``build_hooks.py``, checks the installed cuda-bindings
against the floor. It checks that the ``cuda.h`` that it compiles against is
Expand All @@ -29,7 +33,9 @@
- The ``check-bindings-config`` pre-commit hook checks that each registered
toolkit in ``ci/versions.yml`` is not below its corresponding floor
major.minor. The separate ``check-cuda-core-bindings-floor`` hook validates
the extras and prevents documentation from hardcoding the floors.
the extras, checks that each ``cu<major>`` extra includes its
``minimal-sysctk<major>`` extra, and prevents documentation from hardcoding
the floors.

The build and the tools above load this module by file path. It therefore uses
the standard library only and must not import anything from ``cuda``.
Expand Down Expand Up @@ -58,6 +64,8 @@
_REQUIREMENT_RE = re.compile(
r"^\s*cuda-bindings(?=[\[<>=!~;\s]|$)\s*(?:\[[^\]]*\])?\s*(?P<specifiers>[^;]*?)\s*(?:;.*)?$"
)
# A self-reference such as `cuda-core[minimal-sysctk13]`, up to an optional environment marker.
_SELF_REFERENCE_RE = re.compile(r"^\s*cuda-core\s*\[(?P<extras>[^\]]*)\]\s*(?:;.*)?$")
_FLOOR_SPEC_RE = re.compile(r"^>=(\d+)\.(\d+)\.(\d+)$")
# The upper bound: `<14`, `<14.0` or `<14.0.0`, or the equivalent `==13.*`.
_UPPER_SPEC_RE = re.compile(r"^<(\d+)(?:\.0)*$")
Expand Down Expand Up @@ -94,20 +102,27 @@ def floors_from_extras(extras: Mapping[str, Sequence[str]]) -> dict[int, tuple[i
"""The floor per CUDA major, from the ``cu<major>`` extras of ``pyproject.toml``.

``extras`` is the parsed ``[project.optional-dependencies]`` table. Each
``cu<N>`` extra must list exactly one cuda-bindings requirement with two
``cu<N>`` extra must list exactly one versioned cuda-bindings requirement,
either itself or through a ``cuda-core[<extra>]`` self-reference such as
``cuda-core[minimal-sysctk<N>]``. An unversioned requirement such as
``cuda-bindings[all]`` does not count. The requirement has two
specifiers, in either order. One is the floor, ``>=<N>.<minor>.<patch>``.
The other is an upper bound that excludes the next major, ``<<N+1>`` or
``==<N>.*``. Anything else raises ValueError with the name of the extra, so
a typo fails the build and the pre-commit hook instead of a silent shift of
the floor.
"""
floors: dict[int, tuple[int, int, int]] = {}
for extra, requirements in extras.items():
for extra in extras:
m = _EXTRA_RE.match(extra)
if m is None:
continue
major = int(m.group(1))
matches = [(r, rm) for r in requirements if (rm := _REQUIREMENT_RE.match(r)) is not None]
matches = [
(r, rm)
for r in _expand_self_references(extras, extra)
if (rm := _REQUIREMENT_RE.match(r)) is not None and rm.group("specifiers")
]
if len(matches) != 1:
raise ValueError(
f"the {extra!r} extra must list exactly one cuda-bindings requirement, found {len(matches)}"
Expand All @@ -132,6 +147,21 @@ def floors_from_extras(extras: Mapping[str, Sequence[str]]) -> dict[int, tuple[i
return dict(sorted(floors.items()))


def _expand_self_references(extras: Mapping[str, Sequence[str]], extra: str, seen: tuple[str, ...] = ()) -> list[str]:
"""The requirements of ``extra``, with each ``cuda-core[...]`` replaced by the requirements of the extras it names."""
seen = (*seen, extra)
requirements = []
for requirement in extras.get(extra, ()):
m = _SELF_REFERENCE_RE.match(requirement)
if m is None:
requirements.append(requirement)
continue
for name in (name.strip() for name in m.group("extras").split(",")):
if name and name not in seen:
requirements.extend(_expand_self_references(extras, name, seen))
return requirements


def _confined_major(specifier: str) -> int | None:
"""The one major a specifier confines cuda-bindings to: ``<14`` -> 13, ``==13.*`` -> 13, else None."""
if (m := _UPPER_SPEC_RE.match(specifier)) is not None:
Expand Down
20 changes: 15 additions & 5 deletions cuda_core/docs/source/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,31 @@ Installing from PyPI

``cuda.core`` works with ``cuda-bindings`` (part of ``cuda-python``) 12 or 13 at or above the
release's floor for that major. See :ref:`cuda-core-bindings-floor`. The ``cu12`` and ``cu13``
extras install a compatible version. Test dependencies now use the ``cuda-toolkit`` metapackage for improved dependency resolution. For example with CUDA 12:
extras install a compatible version together with the CUDA Toolkit wheels. Test dependencies now use the ``cuda-toolkit`` metapackage for improved dependency resolution. For example with CUDA 12:

.. code-block:: console

$ pip install cuda-core[cu12]

and likewise use ``[cu13]`` for CUDA 13.
and likewise use ``[cu13]`` for CUDA 13. The ``minimal-sysctk12`` and ``minimal-sysctk13``
extras install only a compatible ``cuda-bindings``, for use with a CUDA Toolkit installed on the
system:

.. code-block:: console

$ pip install cuda-core[minimal-sysctk12]

If you upgrade ``cuda-core`` alone, the installed ``cuda-bindings`` can be older than the new
release requires. ``import cuda.core`` then reports the required version and the ``pip`` command
that installs it.

Do not pin ``cuda-bindings`` in your own requirements. Depend on ``cuda-core`` with the ``cu12``
or ``cu13`` extra, and let ``cuda-core`` choose the ``cuda-bindings`` version. A pin of your own
can conflict with the floor of a later ``cuda-core`` release.
Do not pin ``cuda-bindings`` in your own requirements. In a package that depends on
``cuda-core``, request ``cuda-core[minimal-sysctk12]`` or ``cuda-core[minimal-sysctk13]`` from
your own CUDA 12 and CUDA 13 extras, or ``cuda-core[cu12]`` or ``cuda-core[cu13]`` to also pull
the CUDA Toolkit wheels, and let ``cuda-core`` choose the ``cuda-bindings`` version. A plain
``cuda-core`` requirement does not pass the floor to pip or uv: they can install an older
``cuda-bindings`` next to it, ``pip check`` does not report it, and ``import cuda.core`` fails.
A pin of your own can conflict with the floor of a later ``cuda-core`` release.


Installing from Conda (conda-forge)
Expand Down
22 changes: 22 additions & 0 deletions cuda_core/docs/source/release/1.3.1-notes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0

.. currentmodule:: cuda.core

``cuda.core`` 1.3.1 Release Notes
=================================

Highlights
----------

- New ``minimal-sysctk12`` and ``minimal-sysctk13`` extras install only ``cuda-bindings`` at or
above this release's floor for that CUDA major, without the CUDA Toolkit wheels that ``cu12``
and ``cu13`` add. The ``cu12`` and ``cu13`` extras include them. Packages that depend on
``cuda-core`` should request one of these extras, so that pip and uv see the floor; with a
plain ``cuda-core`` requirement, an older ``cuda-bindings`` is installed and fails at import.
See :ref:`cuda-core-bindings-floor`.
(`#3459 <https://github.com/NVIDIA/cuda-python/issues/3459>`__)

- ``cuda-core`` 1.3.0 is yanked from PyPI. It declared its floors only in the ``cu12`` and
``cu13`` extras, so a resolver that cannot use 1.3.1 could fall back to it next to an older
``cuda-bindings``.
7 changes: 4 additions & 3 deletions cuda_core/docs/source/support.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ For each supported CUDA major version, each ``cuda-core`` release declares a min
``cuda-bindings`` version, its *floor*. The floor is the oldest ``cuda-bindings`` release that
meets two conditions. It was generated from the ``cuda.h`` major.minor that the published
``cuda-core`` wheels are built against. It provides every ``cuda-bindings`` API and fix that
``cuda-core`` needs. The ``cu12`` and ``cu13`` extras of ``cuda-core`` in ``pyproject.toml``
declare the floors of the current release. The build, the import-time check, this page, and CI
all read them from there.
``cuda-core`` needs. The ``minimal-sysctk12`` and ``minimal-sysctk13`` extras of ``cuda-core``
in ``pyproject.toml`` declare the floors of the current release, and the ``cu12`` and ``cu13``
extras include them. The build, the import-time check, this page, and CI all read them from
there. pip and uv see a floor only when ``cuda-core`` is installed with one of these extras.

.. list-table:: ``cuda-bindings`` floors
:header-rows: 1
Expand Down
31 changes: 24 additions & 7 deletions cuda_core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,18 @@ dependencies = [
"numpy",
]

# The cuda-bindings pins below are the cuda-bindings *floors* of this release, one
# per CUDA major. This is the only place that declares them. build_hooks.py, the
# import-time check, the docs and CI read them from here (see
# cuda/core/_bindings_floor.py and the "Bumping the cuda-bindings floor"
# checklist in AGENTS.md). Keep the form `>=<floor>,<<next major>`.
# The cuda-bindings pins in the minimal-sysctk<major> extras are the cuda-bindings
# *floors* of this release, one per CUDA major. This is the only place that
# declares them. Keep the form `>=<floor>,<<next major>`. The cu<major> extras
# include them, so that pip and uv see the floor whichever extra a dependent
# requests. build_hooks.py, the import-time check, the docs and CI read each
# floor through the cu<major> extra (see cuda/core/_bindings_floor.py and the
# "Bumping the cuda-bindings floor" checklist in AGENTS.md).
[project.optional-dependencies]
cu12 = ["cuda-bindings[all]>=12.9.8,<13", "cuda-toolkit==12.*"]
cu13 = ["cuda-bindings[all]>=13.5.0,<14", "cuda-toolkit==13.*"]
minimal-sysctk12 = ["cuda-bindings>=12.9.8,<13"]
minimal-sysctk13 = ["cuda-bindings>=13.5.0,<14"]
cu12 = ["cuda-core[minimal-sysctk12]", "cuda-bindings[all]", "cuda-toolkit==12.*"]
cu13 = ["cuda-core[minimal-sysctk13]", "cuda-bindings[all]", "cuda-toolkit==13.*"]

[dependency-groups]
# Keep build in sync with build-system.requires; install local cuda-pathfinder separately.
Expand Down Expand Up @@ -102,6 +106,19 @@ conflicts = [
{ extra = "cu12" },
{ extra = "cu13" },
],
# uv locks every pair of extras unless told otherwise, and cu<major> includes minimal-sysctk<major>.
[
{ extra = "minimal-sysctk12" },
{ extra = "minimal-sysctk13" },
],
[
{ extra = "cu12" },
{ extra = "minimal-sysctk13" },
],
[
{ extra = "cu13" },
{ extra = "minimal-sysctk12" },
],
[
{ group = "test-cu12" },
{ group = "test-cu13" },
Expand Down
Loading
Loading