Skip to content

Commit 070553f

Browse files
rwgkrparolin
andauthored
feat(pathfinder): centralize CUDA env var handling, prioritize CUDA_PATH over CUDA_HOME (#1801)
* Squash-merge of PR #1519 (rparolin/env_var_improvements) rebased onto main. Adds cuda.pathfinder._utils.env_var_constants with canonical search order, enhances get_cuda_home_or_path() with robust path comparison and caching, and updates documentation across all packages to reflect the new priority. Co-authored-by: Rob Parolin <rparolin@nvidia.com> Made-with: Cursor * replace _get_cuda_paths() with _get_cuda_path() using pathfinder Drop os.pathsep splitting of CUDA_PATH/CUDA_HOME in both build_hooks.py files. Both functions now delegate to get_cuda_home_or_path() from cuda.pathfinder, returning a single path. See #1801 (comment) Made-with: Cursor * treat empty env vars as undefined in get_cuda_home_or_path() See #1801 (comment) for the rationale Made-with: Cursor * fix(pathfinder): clear get_cuda_home_or_path cache in test fixtures Made-with: Cursor * fix(core): update test_build_hooks for _get_cuda_path rename, drop multi-path test Made-with: Cursor * refactor(core): use get_cuda_home_or_path() in test conftest skipif Made-with: Cursor * refactor(core): use get_cuda_home_or_path() in examples Made-with: Cursor * rename get_cuda_home_or_path -> get_cuda_path_or_home Safe: currently an internal-only API (not yet public). Made-with: Cursor * make get_cuda_path_or_home a public API, privatize CUDA_ENV_VARS_ORDERED Export get_cuda_path_or_home from cuda.pathfinder.__init__. External consumers now import from cuda.pathfinder directly. Rename constant to _CUDA_PATH_ENV_VARS_ORDERED and remove all public references to it. Made-with: Cursor * docs(pathfinder): manually edit 1.5.0 release notes, fix RST formatting (Cursor) Made-with: Cursor * Add 1.5.0, 1.4.3, 1.4.2 in cuda_pathfinder/docs/nv-versions.json * docs: clarify that CUDA_PATH/CUDA_HOME priority comes from pathfinder Pathfinder 1.5.0 release notes no longer claim cross-package consistency (that depends on future bindings/core releases). cuda_bindings env var docs now defer to pathfinder release notes for migration guidance. Made-with: Cursor * fix oversights that slipped in when manually editing cuda_pathfinder/docs/nv-versions.json before Discovered via independent review from GPT-5.4 Extra High * fix(pathfinder): change found_via from "CUDA_HOME" to "CUDA_PATH" Aligns the provenance label with the new CUDA_PATH-first priority. The label signals the highest-priority env var name, not necessarily which variable supplied the value. Discovered via independent review from GPT-5.4 Extra High Made-with: Cursor * fix(build): don't import cuda.pathfinder in build_hooks.py The build backends run in an isolated venv created by pyproject-build. Although cuda-pathfinder is listed in build-system.requires and gets installed, the cuda namespace package from backend-path=["."] shadows the installed cuda-pathfinder, making `from cuda.pathfinder import ...` fail with ModuleNotFoundError. This broke all CI wheel builds. Revert _get_cuda_path() to use os.environ.get() directly with CUDA_PATH > CUDA_HOME priority, and remove cuda-pathfinder from build-system.requires (it was not there on main; our PR added it). Made-with: Cursor * Update pathfinder descriptor catalogs for cusparseLt release 0.9.0 * Slightly enhance comment in _get_cuda_path() * Add PR #1806 to 1.5.0-notes.rst * Systematically rename find_in_cuda_home → find_in_cuda_path * add _cuda_headers_available() guard to conftest files Add a helper that skips tests when no CUDA path is set, but asserts that the include/ subdirectory exists when one is — surfacing stale or incomplete toolkit roots at collection time instead of letting them fail later in compilation. Applied in both the root conftest.py and cuda_core/tests/conftest.py. Made-with: Cursor --------- Co-authored-by: Rob Parolin <rparolin@nvidia.com>
1 parent 70ec6a6 commit 070553f

33 files changed

+402
-162
lines changed

conftest.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4+
45
import os
56

67
import pytest
78

9+
from cuda.pathfinder import get_cuda_path_or_home
10+
11+
12+
# Please keep in sync with the copy in cuda_core/tests/conftest.py.
13+
def _cuda_headers_available() -> bool:
14+
"""Return True if CUDA headers are available, False if no CUDA path is set.
15+
16+
Raises AssertionError if a CUDA path is set but has no include/ subdirectory.
17+
"""
18+
cuda_path = get_cuda_path_or_home()
19+
if cuda_path is None:
20+
return False
21+
assert os.path.isdir(os.path.join(cuda_path, "include")), (
22+
f"CUDA path {cuda_path} does not contain an 'include' subdirectory"
23+
)
24+
return True
25+
826

927
def pytest_collection_modifyitems(config, items): # noqa: ARG001
10-
cuda_home = os.environ.get("CUDA_HOME")
28+
have_headers = _cuda_headers_available()
1129
for item in items:
1230
nodeid = item.nodeid.replace("\\", "/")
1331

@@ -31,6 +49,10 @@ def pytest_collection_modifyitems(config, items): # noqa: ARG001
3149
):
3250
item.add_marker(pytest.mark.cython)
3351

34-
# Gate core cython tests on CUDA_HOME
35-
if "core" in item.keywords and not cuda_home:
36-
item.add_marker(pytest.mark.skip(reason="CUDA_HOME not set; skipping core cython tests"))
52+
# Gate core cython tests on CUDA_PATH
53+
if "core" in item.keywords and not have_headers:
54+
item.add_marker(
55+
pytest.mark.skip(
56+
reason="Environment variable CUDA_PATH or CUDA_HOME is not set: skipping core cython tests"
57+
)
58+
)

cuda_bindings/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ To run these tests:
3333

3434
Cython tests are located in `tests/cython` and need to be built. These builds have the same CUDA Toolkit header requirements as [Installing from Source](https://nvidia.github.io/cuda-python/cuda-bindings/latest/install.html#requirements) where the major.minor version must match `cuda.bindings`. To build them:
3535

36-
1. Setup environment variable `CUDA_HOME` with the path to the CUDA Toolkit installation.
36+
1. Setup environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence.
3737
2. Run `build_tests` script located in `test/cython` appropriate to your platform. This will both cythonize the tests and build them.
3838

3939
To run these tests:

cuda_bindings/build_hooks.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@
3434

3535

3636
@functools.cache
37-
def _get_cuda_paths() -> list[str]:
38-
CUDA_HOME = os.environ.get("CUDA_HOME", os.environ.get("CUDA_PATH", None))
39-
if not CUDA_HOME:
40-
raise RuntimeError("Environment variable CUDA_HOME or CUDA_PATH is not set")
41-
CUDA_HOME = CUDA_HOME.split(os.pathsep)
42-
print("CUDA paths:", CUDA_HOME)
43-
return CUDA_HOME
37+
def _get_cuda_path() -> str:
38+
# Not using cuda.pathfinder.get_cuda_path_or_home() here because this
39+
# build backend runs in an isolated venv where the cuda namespace package
40+
# from backend-path shadows the installed cuda-pathfinder. See #1803 for
41+
# a workaround to apply after cuda-pathfinder >= 1.5 is released.
42+
cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME"))
43+
if not cuda_path:
44+
raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set")
45+
print("CUDA path:", cuda_path)
46+
return cuda_path
4447

4548

4649
# -----------------------------------------------------------------------
@@ -133,8 +136,8 @@ def _fetch_header_paths(required_headers, include_path_list):
133136
if missing_headers:
134137
error_message = "Couldn't find required headers: "
135138
error_message += ", ".join(missing_headers)
136-
cuda_paths = _get_cuda_paths()
137-
raise RuntimeError(f'{error_message}\nIs CUDA_HOME setup correctly? (CUDA_HOME="{cuda_paths}")')
139+
cuda_path = _get_cuda_path()
140+
raise RuntimeError(f'{error_message}\nIs CUDA_PATH setup correctly? (CUDA_PATH="{cuda_path}")')
138141

139142
return header_dict
140143

@@ -291,7 +294,7 @@ def _build_cuda_bindings(strip=False):
291294

292295
global _extensions
293296

294-
cuda_paths = _get_cuda_paths()
297+
cuda_path = _get_cuda_path()
295298

296299
if os.environ.get("PARALLEL_LEVEL") is not None:
297300
warn(
@@ -307,7 +310,7 @@ def _build_cuda_bindings(strip=False):
307310
compile_for_coverage = bool(int(os.environ.get("CUDA_PYTHON_COVERAGE", "0")))
308311

309312
# Parse CUDA headers
310-
include_path_list = [os.path.join(path, "include") for path in cuda_paths]
313+
include_path_list = [os.path.join(cuda_path, "include")]
311314
header_dict = _fetch_header_paths(_REQUIRED_HEADERS, include_path_list)
312315
found_types, found_functions, found_values, found_struct, struct_list = _parse_headers(
313316
header_dict, include_path_list, parser_caching
@@ -347,7 +350,7 @@ def _build_cuda_bindings(strip=False):
347350
] + include_path_list
348351
library_dirs = [sysconfig.get_path("platlib"), os.path.join(os.sys.prefix, "lib")]
349352
cudalib_subdirs = [r"lib\x64"] if sys.platform == "win32" else ["lib64", "lib"]
350-
library_dirs.extend(os.path.join(prefix, subdir) for prefix in cuda_paths for subdir in cudalib_subdirs)
353+
library_dirs.extend(os.path.join(cuda_path, subdir) for subdir in cudalib_subdirs)
351354

352355
extra_compile_args = []
353356
extra_link_args = []

cuda_bindings/docs/source/environment_variables.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ Runtime Environment Variables
1515
Build-Time Environment Variables
1616
--------------------------------
1717

18-
- ``CUDA_HOME`` or ``CUDA_PATH``: Specifies the location of the CUDA Toolkit.
18+
- ``CUDA_PATH`` or ``CUDA_HOME``: Specifies the location of the CUDA Toolkit. If both are set, ``CUDA_PATH`` takes precedence.
19+
20+
.. note::
21+
The ``CUDA_PATH`` > ``CUDA_HOME`` priority is determined by ``cuda-pathfinder``.
22+
Earlier versions of ``cuda-pathfinder`` (before 1.5.0) used the opposite order
23+
(``CUDA_HOME`` > ``CUDA_PATH``). See the
24+
`cuda-pathfinder 1.5.0 release notes <https://nvidia.github.io/cuda-python/cuda-pathfinder/latest/release/1.5.0-notes.html>`_
25+
for details and migration guidance.
1926

2027
- ``CUDA_PYTHON_PARSER_CACHING`` : bool, toggles the caching of parsed header files during the cuda-bindings build process. If caching is enabled (``CUDA_PYTHON_PARSER_CACHING`` is True), the cache path is set to ./cache_<library_name>, where <library_name> is derived from the cuda toolkit libraries used to build cuda-bindings.
2128

cuda_bindings/docs/source/install.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ Requirements
8787

8888
[^2]: The CUDA Runtime static library (``libcudart_static.a`` on Linux, ``cudart_static.lib`` on Windows) is part of the CUDA Toolkit. If using conda packages, it is contained in the ``cuda-cudart-static`` package.
8989

90-
Source builds require that the provided CUDA headers are of the same major.minor version as the ``cuda.bindings`` you're trying to build. Despite this requirement, note that the minor version compatibility is still maintained. Use the ``CUDA_HOME`` (or ``CUDA_PATH``) environment variable to specify the location of your headers. For example, if your headers are located in ``/usr/local/cuda/include``, then you should set ``CUDA_HOME`` with:
90+
Source builds require that the provided CUDA headers are of the same major.minor version as the ``cuda.bindings`` you're trying to build. Despite this requirement, note that the minor version compatibility is still maintained. Use the ``CUDA_PATH`` (or ``CUDA_HOME``) environment variable to specify the location of your headers. If both are set, ``CUDA_PATH`` takes precedence. For example, if your headers are located in ``/usr/local/cuda/include``, then you should set ``CUDA_PATH`` with:
9191

9292
.. code-block:: console
9393
94-
$ export CUDA_HOME=/usr/local/cuda
94+
$ export CUDA_PATH=/usr/local/cuda
9595
9696
See `Environment Variables <environment_variables.rst>`_ for a description of other build-time environment variables.
9797

cuda_core/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Alternatively, from the repository root you can use a simple script:
2626

2727
Cython tests are located in `tests/cython` and need to be built. These builds have the same CUDA Toolkit header requirements as [those of cuda.bindings](https://nvidia.github.io/cuda-python/cuda-bindings/latest/install.html#requirements) where the major.minor version must match `cuda.bindings`. To build them:
2828

29-
1. Set up environment variable `CUDA_HOME` with the path to the CUDA Toolkit installation.
29+
1. Set up environment variable `CUDA_PATH` (or `CUDA_HOME`) with the path to the CUDA Toolkit installation. Note: If both are set, `CUDA_PATH` takes precedence.
3030
2. Run `build_tests` script located in `tests/cython` appropriate to your platform. This will both cythonize the tests and build them.
3131

3232
To run these tests:

cuda_core/build_hooks.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@
2929

3030

3131
@functools.cache
32-
def _get_cuda_paths() -> list[str]:
33-
cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME", None))
32+
def _get_cuda_path() -> str:
33+
# Not using cuda.pathfinder.get_cuda_path_or_home() here because this
34+
# build backend runs in an isolated venv where the cuda namespace package
35+
# from backend-path shadows the installed cuda-pathfinder. See #1803 for
36+
# a workaround to apply after cuda-pathfinder >= 1.5 is released.
37+
cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME"))
3438
if not cuda_path:
3539
raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set")
36-
cuda_path = cuda_path.split(os.pathsep)
37-
print("CUDA paths:", cuda_path)
40+
print("CUDA path:", cuda_path)
3841
return cuda_path
3942

4043

@@ -60,21 +63,20 @@ def _determine_cuda_major_version() -> str:
6063
return cuda_major
6164

6265
# Derive from the CUDA headers (the authoritative source for what we compile against).
63-
cuda_path = _get_cuda_paths()
64-
for root in cuda_path:
65-
cuda_h = os.path.join(root, "include", "cuda.h")
66-
try:
67-
with open(cuda_h, encoding="utf-8") as f:
68-
for line in f:
69-
m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line)
70-
if m:
71-
v = int(m.group(1))
72-
# CUDA_VERSION is e.g. 12020 for 12.2.
73-
cuda_major = str(v // 1000)
74-
print("CUDA MAJOR VERSION:", cuda_major)
75-
return cuda_major
76-
except OSError:
77-
continue
66+
cuda_path = _get_cuda_path()
67+
cuda_h = os.path.join(cuda_path, "include", "cuda.h")
68+
try:
69+
with open(cuda_h, encoding="utf-8") as f:
70+
for line in f:
71+
m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line)
72+
if m:
73+
v = int(m.group(1))
74+
# CUDA_VERSION is e.g. 12020 for 12.2.
75+
cuda_major = str(v // 1000)
76+
print("CUDA MAJOR VERSION:", cuda_major)
77+
return cuda_major
78+
except OSError:
79+
pass
7880

7981
# CUDA_PATH or CUDA_HOME is required for the build, so we should not reach here
8082
# in normal circumstances. Raise an error to make the issue clear.
@@ -132,7 +134,7 @@ def get_sources(mod_name):
132134

133135
return sources
134136

135-
all_include_dirs = [os.path.join(root, "include") for root in _get_cuda_paths()]
137+
all_include_dirs = [os.path.join(_get_cuda_path(), "include")]
136138
extra_compile_args = []
137139
if COMPILE_FOR_COVERAGE:
138140
# CYTHON_TRACE_NOGIL indicates to trace nogil functions. It is not

cuda_core/examples/thread_block_cluster.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ProgramOptions,
2424
launch,
2525
)
26+
from cuda.pathfinder import get_cuda_path_or_home
2627

2728
# print cluster info using a kernel and store results in pinned memory
2829
code = r"""
@@ -65,9 +66,9 @@ def main():
6566
print("This example requires NumPy 2.2.5 or later", file=sys.stderr)
6667
sys.exit(1)
6768

68-
cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME"))
69+
cuda_path = get_cuda_path_or_home()
6970
if cuda_path is None:
70-
print("this example requires a valid CUDA_PATH environment variable set", file=sys.stderr)
71+
print("This example requires CUDA_PATH or CUDA_HOME to point to a CUDA toolkit.", file=sys.stderr)
7172
sys.exit(1)
7273
cuda_include = os.path.join(cuda_path, "include")
7374
if not os.path.isdir(cuda_include):

cuda_core/examples/tma_tensor_map.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
StridedMemoryView,
3737
launch,
3838
)
39+
from cuda.pathfinder import get_cuda_path_or_home
3940

4041
# ---------------------------------------------------------------------------
4142
# CUDA kernel that uses TMA to load a 1-D tile into shared memory, then
@@ -103,7 +104,7 @@
103104

104105

105106
def _get_cccl_include_paths():
106-
cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME"))
107+
cuda_path = get_cuda_path_or_home()
107108
if cuda_path is None:
108109
print("This example requires CUDA_PATH or CUDA_HOME to point to a CUDA toolkit.", file=sys.stderr)
109110
sys.exit(1)

cuda_core/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
requires = [
77
"setuptools>=80",
88
"setuptools-scm[simple]>=8",
9-
"Cython>=3.2,<3.3"
9+
"Cython>=3.2,<3.3",
1010
]
1111
build-backend = "build_hooks"
1212
backend-path = ["."]

0 commit comments

Comments
 (0)