From 8ce5d3aca9856200a777869089f26a05b77b7507 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Wed, 21 Jan 2026 17:03:53 -0800 Subject: [PATCH 01/13] initial commit --- conftest.py | 11 +- cuda_bindings/README.md | 2 +- .../docs/source/environment_variables.rst | 17 ++- cuda_bindings/docs/source/install.rst | 8 +- cuda_bindings/examples/common/common.py | 12 +- cuda_bindings/pixi.lock | 33 ++--- cuda_bindings/pixi.toml | 1 + cuda_bindings/setup.py | 20 ++- cuda_core/README.md | 2 +- cuda_core/build_hooks.py | 52 +++++--- cuda_core/examples/thread_block_cluster.py | 4 +- cuda_core/tests/helpers/__init__.py | 3 +- cuda_core/tests/test_build_hooks.py | 6 +- .../_dynamic_libs/load_nvidia_dynamic_lib.py | 5 +- .../_headers/find_nvidia_headers.py | 3 +- .../pathfinder/_utils/env_var_constants.py | 33 +++++ .../cuda/pathfinder/_utils/env_vars.py | 101 +++++++++++--- cuda_pathfinder/docs/source/api.rst | 16 +++ .../docs/source/release/1.4.0-notes.rst | 49 +++++++ cuda_pathfinder/tests/test_utils_env_vars.py | 125 ++++++++++++++---- 20 files changed, 398 insertions(+), 105 deletions(-) create mode 100644 cuda_pathfinder/cuda/pathfinder/_utils/env_var_constants.py create mode 100644 cuda_pathfinder/docs/source/release/1.4.0-notes.rst diff --git a/conftest.py b/conftest.py index 7c4cf3a761..917a7ebc5b 100644 --- a/conftest.py +++ b/conftest.py @@ -5,9 +5,18 @@ import pytest +# Import centralized CUDA environment variable handling +try: + from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +except ImportError as e: + raise ImportError( + "Failed to import cuda.pathfinder. " + "Please ensure cuda-pathfinder is installed: pip install cuda-pathfinder" + ) from e + def pytest_collection_modifyitems(config, items): - cuda_home = os.environ.get("CUDA_HOME") + cuda_home = get_cuda_home_or_path() for item in items: nodeid = item.nodeid.replace("\\", "/") diff --git a/cuda_bindings/README.md b/cuda_bindings/README.md index a0657706d0..f1df49b67e 100644 --- a/cuda_bindings/README.md +++ b/cuda_bindings/README.md @@ -33,7 +33,7 @@ To run these tests: 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: -1. Setup environment variable `CUDA_HOME` with the path to the CUDA Toolkit installation. +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 (see `cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). 2. Run `build_tests` script located in `test/cython` appropriate to your platform. This will both cythonize the tests and build them. To run these tests: diff --git a/cuda_bindings/docs/source/environment_variables.rst b/cuda_bindings/docs/source/environment_variables.rst index a212bfe764..8c79649989 100644 --- a/cuda_bindings/docs/source/environment_variables.rst +++ b/cuda_bindings/docs/source/environment_variables.rst @@ -13,7 +13,22 @@ Runtime Environment Variables Build-Time Environment Variables -------------------------------- -- ``CUDA_HOME`` or ``CUDA_PATH``: Specifies the location of the CUDA Toolkit. +- ``CUDA_PATH`` or ``CUDA_HOME``: Specifies the location of the CUDA Toolkit. If both are set, ``CUDA_PATH`` takes precedence. This search order is defined in :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`. + + .. note:: + **Breaking Change in v1.4.0**: The priority order changed from ``CUDA_HOME`` > ``CUDA_PATH`` to ``CUDA_PATH`` > ``CUDA_HOME``. + + **Migration Guide**: + + - If you only set one variable, no changes are needed + - If you set both variables to the same location, no changes are needed + - If you set both variables to different locations and relied on ``CUDA_HOME`` taking precedence, you should either: + + - Switch to using only ``CUDA_PATH`` (recommended) + - Ensure both variables point to the same CUDA Toolkit installation + - Be aware that ``CUDA_PATH`` will now be used + + A warning will be issued if both variables are set but point to different locations. - ``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_, where is derived from the cuda toolkit libraries used to build cuda-bindings. diff --git a/cuda_bindings/docs/source/install.rst b/cuda_bindings/docs/source/install.rst index 58a6a0f31c..26d527cd02 100644 --- a/cuda_bindings/docs/source/install.rst +++ b/cuda_bindings/docs/source/install.rst @@ -87,11 +87,15 @@ Requirements [^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. -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: +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 (see :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). For example, if your headers are located in ``/usr/local/cuda/include``, then you should set ``CUDA_PATH`` with: .. code-block:: console - $ export CUDA_HOME=/usr/local/cuda + $ export CUDA_PATH=/usr/local/cuda + +.. note:: + + The CUDA Toolkit path is determined once at the start of the build process and cached. If you need to change the path during development, restart your build environment. See `Environment Variables `_ for a description of other build-time environment variables. diff --git a/cuda_bindings/examples/common/common.py b/cuda_bindings/examples/common/common.py index 13b57749a6..26ca0e535f 100644 --- a/cuda_bindings/examples/common/common.py +++ b/cuda_bindings/examples/common/common.py @@ -8,19 +8,13 @@ from cuda.bindings import driver as cuda from cuda.bindings import nvrtc from cuda.bindings import runtime as cudart - - -def get_cuda_home(): - cuda_home = os.getenv("CUDA_HOME") - if cuda_home is None: - cuda_home = os.getenv("CUDA_PATH") - return cuda_home +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path def pytest_skipif_cuda_include_not_found(): import pytest - cuda_home = get_cuda_home() + cuda_home = get_cuda_home_or_path() if cuda_home is None: pytest.skip("CUDA_HOME/CUDA_PATH not set") cuda_include = os.path.join(cuda_home, "include") @@ -46,7 +40,7 @@ class KernelHelper: def __init__(self, code, devID): prog = checkCudaErrors(nvrtc.nvrtcCreateProgram(str.encode(code), b"sourceCode.cu", 0, None, None)) - cuda_home = get_cuda_home() + cuda_home = get_cuda_home_or_path() assert cuda_home is not None cuda_include = os.path.join(cuda_home, "include") assert os.path.isdir(cuda_include) diff --git a/cuda_bindings/pixi.lock b/cuda_bindings/pixi.lock index fb3d0ad393..3a9ae219de 100644 --- a/cuda_bindings/pixi.lock +++ b/cuda_bindings/pixi.lock @@ -515,7 +515,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 default: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -1031,7 +1031,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -1473,11 +1473,11 @@ packages: - conda: . name: cuda-bindings version: 13.1.0 - build: py314h625260f_0 - subdir: win-64 + build: py314ha479ada_0 + subdir: linux-aarch64 variants: python: 3.14.* - target_platform: win-64 + target_platform: linux-aarch64 depends: - python - cuda-pathfinder >=1.1,<2 @@ -1485,18 +1485,23 @@ packages: - cuda-nvrtc - cuda-nvrtc >=13.1.115,<14.0a0 - cuda-nvvm - - vc >=14.1,<15 - - vc14_runtime >=14.16.27033 + - libcufile + - libcufile >=1.16.1.26,<2.0a0 + - libgcc >=15 + - libgcc >=15 + - libstdcxx >=15 - python_abi 3.14.* *_cp314 license: LicenseRef-NVIDIA-SOFTWARE-LICENSE - conda: . name: cuda-bindings version: 13.1.0 - build: py314ha479ada_0 - subdir: linux-aarch64 + build: py314hae7e39d_0 + subdir: win-64 variants: + c_compiler: vs2022 + cxx_compiler: vs2022 python: 3.14.* - target_platform: linux-aarch64 + target_platform: win-64 depends: - python - cuda-pathfinder >=1.1,<2 @@ -1504,11 +1509,9 @@ packages: - cuda-nvrtc - cuda-nvrtc >=13.1.115,<14.0a0 - cuda-nvvm - - libcufile - - libcufile >=1.16.1.26,<2.0a0 - - libgcc >=15 - - libgcc >=15 - - libstdcxx >=15 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 - python_abi 3.14.* *_cp314 license: LicenseRef-NVIDIA-SOFTWARE-LICENSE - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-cccl_linux-64-13.1.78-ha770c72_0.conda diff --git a/cuda_bindings/pixi.toml b/cuda_bindings/pixi.toml index 44e320d6d3..126cee7ceb 100644 --- a/cuda_bindings/pixi.toml +++ b/cuda_bindings/pixi.toml @@ -95,6 +95,7 @@ setuptools = ">=80" setuptools-scm = ">=8" cython = ">=3.2,<3.3" pyclibrary = ">=0.1.7" +cuda-pathfinder = ">=1.1,<2" cuda-cudart-static = "*" cuda-nvrtc-dev = "*" cuda-profiler-api = "*" diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index bfa1ae7826..59080e39f9 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -23,14 +23,22 @@ from setuptools.command.editable_wheel import _TopLevelFinder, editable_wheel from setuptools.extension import Extension +# Note: cuda_bindings requires cuda.pathfinder to be installed to ensure consistent +# environment variable handling across all CUDA Python packages. +try: + from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +except ImportError as e: + raise RuntimeError( + "cuda.pathfinder package is required to build cuda_bindings. " + "Please install it first: pip install cuda-pathfinder" + ) from e + # ---------------------------------------------------------------------- # Fetch configuration options -CUDA_HOME = os.environ.get("CUDA_HOME", os.environ.get("CUDA_PATH", None)) +CUDA_HOME = get_cuda_home_or_path() if not CUDA_HOME: - raise RuntimeError("Environment variable CUDA_HOME or CUDA_PATH is not set") - -CUDA_HOME = CUDA_HOME.split(os.pathsep) + raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") if os.environ.get("PARALLEL_LEVEL") is not None: warn( @@ -207,7 +215,7 @@ def parse_headers(header_dict): return found_types, found_functions, found_values, found_struct, struct_list -include_path_list = [os.path.join(path, "include") for path in CUDA_HOME] +include_path_list = [os.path.join(CUDA_HOME, "include")] header_dict = fetch_header_paths(required_headers, include_path_list) found_types, found_functions, found_values, found_struct, struct_list = parse_headers(header_dict) @@ -260,7 +268,7 @@ def generate_output(infile, local): ] + include_path_list library_dirs = [sysconfig.get_path("platlib"), os.path.join(os.sys.prefix, "lib")] cudalib_subdirs = [r"lib\x64"] if sys.platform == "win32" else ["lib64", "lib"] -library_dirs.extend(os.path.join(prefix, subdir) for prefix in CUDA_HOME for subdir in cudalib_subdirs) +library_dirs.extend(os.path.join(CUDA_HOME, subdir) for subdir in cudalib_subdirs) extra_compile_args = [] extra_cythonize_kwargs = {} diff --git a/cuda_core/README.md b/cuda_core/README.md index 9925511ef9..aa2d1c3fbe 100644 --- a/cuda_core/README.md +++ b/cuda_core/README.md @@ -26,7 +26,7 @@ Alternatively, from the repository root you can use a simple script: 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: -1. Set up environment variable `CUDA_HOME` with the path to the CUDA Toolkit installation. +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 (see `cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). 2. Run `build_tests` script located in `tests/cython` appropriate to your platform. This will both cythonize the tests and build them. To run these tests: diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index bb7951db62..9fe039e2af 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -16,6 +16,20 @@ from setuptools import Extension from setuptools import build_meta as _build_meta +# Import centralized CUDA environment variable handling +# Note: This import may fail at build-dependency-resolution time if cuda-pathfinder +# is not yet installed, but it's guaranteed to be available when _get_cuda_path() +# is actually called (during wheel build time). +try: + from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path +except ImportError as e: + raise ImportError( + "Failed to import cuda.pathfinder. " + "Please ensure cuda-pathfinder is installed as a build dependency. " + "If building cuda-core, cuda-pathfinder should be automatically installed. " + "If this error persists, try: pip install cuda-pathfinder" + ) from e + prepare_metadata_for_build_editable = _build_meta.prepare_metadata_for_build_editable prepare_metadata_for_build_wheel = _build_meta.prepare_metadata_for_build_wheel build_sdist = _build_meta.build_sdist @@ -25,12 +39,11 @@ @functools.cache -def _get_cuda_paths() -> list[str]: - CUDA_PATH = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME", None)) +def _get_cuda_path() -> str: + CUDA_PATH = get_cuda_home_or_path() if not CUDA_PATH: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") - CUDA_PATH = CUDA_PATH.split(os.pathsep) - print("CUDA paths:", CUDA_PATH) + print("CUDA path:", CUDA_PATH) return CUDA_PATH @@ -56,21 +69,20 @@ def _determine_cuda_major_version() -> str: return cuda_major # Derive from the CUDA headers (the authoritative source for what we compile against). - cuda_path = _get_cuda_paths() - for root in cuda_path: - cuda_h = os.path.join(root, "include", "cuda.h") - try: - with open(cuda_h, encoding="utf-8") as f: - for line in f: - m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line) - if m: - v = int(m.group(1)) - # CUDA_VERSION is e.g. 12020 for 12.2. - cuda_major = str(v // 1000) - print("CUDA MAJOR VERSION:", cuda_major) - return cuda_major - except OSError: - continue + cuda_path = _get_cuda_path() + cuda_h = os.path.join(cuda_path, "include", "cuda.h") + try: + with open(cuda_h, encoding="utf-8") as f: + for line in f: + m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line) + if m: + v = int(m.group(1)) + # CUDA_VERSION is e.g. 12020 for 12.2. + cuda_major = str(v // 1000) + print("CUDA MAJOR VERSION:", cuda_major) + return cuda_major + except OSError: + pass # CUDA_PATH or CUDA_HOME is required for the build, so we should not reach here # in normal circumstances. Raise an error to make the issue clear. @@ -112,7 +124,7 @@ def get_sources(mod_name): return sources - all_include_dirs = list(os.path.join(root, "include") for root in _get_cuda_paths()) + all_include_dirs = [os.path.join(_get_cuda_path(), "include")] extra_compile_args = [] if COMPILE_FOR_COVERAGE: # CYTHON_TRACE_NOGIL indicates to trace nogil functions. It is not diff --git a/cuda_core/examples/thread_block_cluster.py b/cuda_core/examples/thread_block_cluster.py index f1ea8b8579..dbc04d2289 100644 --- a/cuda_core/examples/thread_block_cluster.py +++ b/cuda_core/examples/thread_block_cluster.py @@ -27,7 +27,9 @@ sys.exit(0) # prepare include -cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME")) +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + +cuda_path = get_cuda_home_or_path() if cuda_path is None: print("this demo requires a valid CUDA_PATH environment variable set", file=sys.stderr) sys.exit(0) diff --git a/cuda_core/tests/helpers/__init__.py b/cuda_core/tests/helpers/__init__.py index ad9d281c16..a7a1afcff9 100644 --- a/cuda_core/tests/helpers/__init__.py +++ b/cuda_core/tests/helpers/__init__.py @@ -8,8 +8,9 @@ from typing import Union from cuda.core._utils.cuda_utils import handle_return +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path -CUDA_PATH = os.environ.get("CUDA_PATH") +CUDA_PATH = get_cuda_home_or_path() CUDA_INCLUDE_PATH = None CCCL_INCLUDE_PATHS = None if CUDA_PATH is not None: diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index e416503bc0..e82ee67e84 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -65,7 +65,7 @@ def _check_version_detection( cuda_h = include_dir / "cuda.h" cuda_h.write_text(f"#define CUDA_VERSION {cuda_version}\n") - build_hooks._get_cuda_paths.cache_clear() + build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() mock_env = { @@ -89,7 +89,7 @@ class TestGetCudaMajorVersion: @pytest.mark.parametrize("version", ["11", "12", "13", "14"]) def test_env_var_override(self, version): """CUDA_CORE_BUILD_MAJOR env var override works with various versions.""" - build_hooks._get_cuda_paths.cache_clear() + build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() with mock.patch.dict(os.environ, {"CUDA_CORE_BUILD_MAJOR": version}, clear=False): result = build_hooks._determine_cuda_major_version() @@ -122,7 +122,7 @@ def test_env_var_takes_priority_over_headers(self): def test_missing_cuda_path_raises_error(self): """RuntimeError is raised when CUDA_PATH/CUDA_HOME not set and no env var override.""" - build_hooks._get_cuda_paths.cache_clear() + build_hooks._get_cuda_path.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() with ( mock.patch.dict(os.environ, {}, clear=True), diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py index 3431c2f86b..5724a054bd 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py @@ -54,7 +54,7 @@ def _load_lib_no_cache(libname: str) -> LoadedDL: if abs_path is None: finder.raise_not_found_error() else: - found_via = "CUDA_HOME" + found_via = "CUDA_PATH" return load_with_abs_path(libname, abs_path, found_via) @@ -121,7 +121,8 @@ def load_nvidia_dynamic_lib(libname: str) -> LoadedDL: 4. **Environment variables** - - If set, use ``CUDA_HOME`` or ``CUDA_PATH`` (in that order). + - If set, use ``CUDA_PATH`` or ``CUDA_HOME`` (in that order, as defined by + :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). Notes: The search is performed **per library**. There is currently no mechanism to diff --git a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py index 63f8a627fd..5838c99574 100644 --- a/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py +++ b/cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py @@ -123,7 +123,8 @@ def find_nvidia_header_directory(libname: str) -> str | None: 3. **CUDA Toolkit environment variables** - - Use ``CUDA_HOME`` or ``CUDA_PATH`` (in that order). + - Use ``CUDA_PATH`` or ``CUDA_HOME`` (in that order, as defined by + :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED`). """ if libname in supported_nvidia_headers.SUPPORTED_HEADERS_CTK: diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_var_constants.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_var_constants.py new file mode 100644 index 0000000000..38ed1fc3ef --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_var_constants.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Constants for CUDA environment variable handling. + +This module defines the canonical search order for CUDA Toolkit environment variables +without any dependencies. It can be safely imported during bootstrap scenarios. + +Search Order Priority: + 1. CUDA_PATH (higher priority) + 2. CUDA_HOME (lower priority) + +.. versionadded:: 1.4.0 + Added centralized environment variable handling. + +.. versionchanged:: 1.4.0 + **Breaking Change**: Priority changed from CUDA_HOME > CUDA_PATH to CUDA_PATH > CUDA_HOME. +""" + +#: Canonical search order for CUDA Toolkit environment variables. +#: +#: This tuple defines the priority order used by :py:func:`~cuda.pathfinder._utils.env_vars.get_cuda_home_or_path` +#: and throughout cuda-python packages when determining which CUDA Toolkit to use. +#: +#: The first variable in the tuple has the highest priority. If multiple variables are set +#: and point to different locations, the first one is used and a warning is issued. +#: +#: .. note:: +#: **Breaking Change in v1.4.0**: The order changed from ``("CUDA_HOME", "CUDA_PATH")`` +#: to ``("CUDA_PATH", "CUDA_HOME")``, making ``CUDA_PATH`` the highest priority. +#: +#: :type: tuple[str, ...] +CUDA_ENV_VARS_ORDERED = ("CUDA_PATH", "CUDA_HOME") diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index cf78a627cb..f7dea9b9e5 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -1,9 +1,30 @@ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +"""Centralized CUDA environment variable handling. + +This module defines the canonical search order for CUDA Toolkit environment variables +used throughout cuda-python packages (cuda.pathfinder, cuda.core, cuda.bindings). + +Search Order Priority: + 1. CUDA_PATH (higher priority) + 2. CUDA_HOME (lower priority) + +If both are set and differ, CUDA_PATH takes precedence and a warning is issued. + +Important Note on Caching: + The result of get_cuda_home_or_path() is cached for the process lifetime. The first + call determines the CUDA Toolkit path, and all subsequent calls return the cached + value, even if environment variables change later. This ensures consistent behavior + throughout the application lifecycle. +""" + +import functools import os import warnings +from .env_var_constants import CUDA_ENV_VARS_ORDERED + def _paths_differ(a: str, b: str) -> bool: """ @@ -32,20 +53,68 @@ def _paths_differ(a: str, b: str) -> bool: return True +@functools.cache def get_cuda_home_or_path() -> str | None: - cuda_home = os.environ.get("CUDA_HOME") - cuda_path = os.environ.get("CUDA_PATH") - - if cuda_home and cuda_path and _paths_differ(cuda_home, cuda_path): - warnings.warn( - "Both CUDA_HOME and CUDA_PATH are set but differ:\n" - f" CUDA_HOME={cuda_home}\n" - f" CUDA_PATH={cuda_path}\n" - "Using CUDA_HOME (higher priority).", - UserWarning, - stacklevel=2, - ) - - if cuda_home is not None: - return cuda_home - return cuda_path + """Get CUDA Toolkit path from environment variables. + + Returns the value of CUDA_PATH or CUDA_HOME following the canonical search order + defined in CUDA_ENV_VARS_ORDERED. If both are set and differ, CUDA_PATH takes + precedence and a warning is issued. + + The result is cached for the process lifetime. The first call determines the CUDA + Toolkit path, and subsequent calls return the cached value. + + Returns: + Path to CUDA Toolkit, or None if neither variable is set. Empty strings are + preserved and returned as-is if explicitly set in the environment. + + Warnings: + UserWarning: If multiple CUDA environment variables are set but point to + different locations (only on the first call). + + See Also: + CUDA_ENV_VARS_ORDERED: The canonical search order for CUDA environment variables. + """ + # Collect all set environment variables in priority order + # Note: We check 'is not None' to preserve empty strings (which are valid but unusual). + # Empty strings are falsy in Python but may indicate an intentional "unset" by the user. + set_vars = {} + for var in CUDA_ENV_VARS_ORDERED: + val = os.environ.get(var) + if val is not None: + set_vars[var] = val + + if not set_vars: + return None + + # If multiple variables are set, check if they differ and warn + if len(set_vars) > 1: + # Check if any non-empty values actually differ + non_empty_values = [(var, val) for var, val in set_vars.items() if val] + + if len(non_empty_values) > 1: + # Check if any pair of non-empty values differs + values_differ = False + for i in range(len(non_empty_values) - 1): + if _paths_differ(non_empty_values[i][1], non_empty_values[i + 1][1]): + values_differ = True + break + + if values_differ: + # Build a generic warning message that works for any number of variables + var_list = "\n".join(f" {var}={val}" for var, val in set_vars.items()) + highest_priority = CUDA_ENV_VARS_ORDERED[0] + warnings.warn( + f"Multiple CUDA environment variables are set but differ:\n" + f"{var_list}\n" + f"Using {highest_priority} (highest priority as defined in CUDA_ENV_VARS_ORDERED).", + UserWarning, + stacklevel=2, + ) + + # Return the first (highest priority) set variable + for var in CUDA_ENV_VARS_ORDERED: + if var in set_vars: + return set_vars[var] + + return None diff --git a/cuda_pathfinder/docs/source/api.rst b/cuda_pathfinder/docs/source/api.rst index 72e5e40724..211aad25f8 100644 --- a/cuda_pathfinder/docs/source/api.rst +++ b/cuda_pathfinder/docs/source/api.rst @@ -20,3 +20,19 @@ and experimental APIs for locating NVIDIA C/C++ header directories. SUPPORTED_HEADERS_CTK SUPPORTED_HEADERS_NON_CTK find_nvidia_header_directory + +Environment Variable Utilities +------------------------------- + +The ``cuda.pathfinder._utils.env_vars`` module provides centralized handling of CUDA +environment variables used across all cuda-python packages. + +.. autosummary:: + :toctree: generated/ + + _utils.env_vars.get_cuda_home_or_path + _utils.env_vars.CUDA_ENV_VARS_ORDERED + +.. autofunction:: cuda.pathfinder._utils.env_vars.get_cuda_home_or_path + +.. autodata:: cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED diff --git a/cuda_pathfinder/docs/source/release/1.4.0-notes.rst b/cuda_pathfinder/docs/source/release/1.4.0-notes.rst new file mode 100644 index 0000000000..32d45aac03 --- /dev/null +++ b/cuda_pathfinder/docs/source/release/1.4.0-notes.rst @@ -0,0 +1,49 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +.. py:currentmodule:: cuda.pathfinder + +``cuda-pathfinder`` 1.4.0 Release notes +======================================= + +Released on TBD + +Highlights +---------- + +Breaking Changes +~~~~~~~~~~~~~~~~ + +* **CUDA environment variable priority changed**: ``CUDA_PATH`` now takes precedence over ``CUDA_HOME`` when both are set. Previously, ``CUDA_HOME`` had higher priority. If both variables are set and point to different locations, a warning will be issued and ``CUDA_PATH`` will be used. This change aligns with industry standards and NVIDIA's recommended practices. + + **Migration Guide**: + + - If you rely on ``CUDA_HOME``, consider switching to ``CUDA_PATH`` + - If you set both variables, ensure they point to the same CUDA Toolkit installation + - If they differ intentionally, be aware that ``CUDA_PATH`` will now be used + - The canonical search order is defined in :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED` + +New Features +~~~~~~~~~~~~ + +* Added centralized CUDA environment variable handling with :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_home_or_path()`. This function provides: + + - Consistent behavior across all cuda-python packages (cuda.pathfinder, cuda.core, cuda.bindings) + - Intelligent path comparison that handles symlinks, case sensitivity, and trailing slashes + - Result caching for performance (first call determines the path for the process lifetime) + - Clear warnings when multiple environment variables are set but differ + +* Added :py:data:`cuda.pathfinder._utils.env_vars.CUDA_ENV_VARS_ORDERED` constant that explicitly documents the canonical search order for CUDA environment variables. + +Documentation +~~~~~~~~~~~~~ + +* Updated documentation across all packages to reflect the new ``CUDA_PATH`` priority +* Added detailed caching behavior documentation for :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_home_or_path()` +* Clarified environment variable precedence in installation guides + +Bug Fixes +~~~~~~~~~ + +* Improved robustness of test collection by adding fallback when cuda.pathfinder is not yet installed +* Enhanced path comparison logic to properly handle edge cases (nonexistent paths, symlinks, OS-specific behavior) diff --git a/cuda_pathfinder/tests/test_utils_env_vars.py b/cuda_pathfinder/tests/test_utils_env_vars.py index 40c7d4930d..50351565c0 100644 --- a/cuda_pathfinder/tests/test_utils_env_vars.py +++ b/cuda_pathfinder/tests/test_utils_env_vars.py @@ -8,7 +8,11 @@ import pytest -from cuda.pathfinder._utils.env_vars import _paths_differ, get_cuda_home_or_path +from cuda.pathfinder._utils.env_vars import ( + CUDA_ENV_VARS_ORDERED, + _paths_differ, + get_cuda_home_or_path, +) skip_symlink_tests = pytest.mark.skipif( sys.platform == "win32", @@ -20,6 +24,8 @@ def unset_env(monkeypatch): """Helper to clear both env vars for each test.""" monkeypatch.delenv("CUDA_HOME", raising=False) monkeypatch.delenv("CUDA_PATH", raising=False) + # Clear the cache so each test gets fresh behavior + get_cuda_home_or_path.cache_clear() def test_returns_none_when_unset(monkeypatch): @@ -27,14 +33,15 @@ def test_returns_none_when_unset(monkeypatch): assert get_cuda_home_or_path() is None -def test_empty_cuda_home_preserved(monkeypatch): +def test_empty_cuda_path_preserved(monkeypatch): # empty string is returned as-is if set. - monkeypatch.setenv("CUDA_HOME", "") - monkeypatch.setenv("CUDA_PATH", "/does/not/matter") + unset_env(monkeypatch) + monkeypatch.setenv("CUDA_PATH", "") + monkeypatch.setenv("CUDA_HOME", "/does/not/matter") assert get_cuda_home_or_path() == "" -def test_prefers_cuda_home_over_cuda_path(monkeypatch, tmp_path): +def test_prefers_cuda_path_over_cuda_home(monkeypatch, tmp_path): unset_env(monkeypatch) home = tmp_path / "home" path = tmp_path / "path" @@ -44,18 +51,18 @@ def test_prefers_cuda_home_over_cuda_path(monkeypatch, tmp_path): monkeypatch.setenv("CUDA_HOME", str(home)) monkeypatch.setenv("CUDA_PATH", str(path)) - # Different directories -> warning + prefer CUDA_HOME - with pytest.warns(UserWarning, match="Both CUDA_HOME and CUDA_PATH are set but differ"): + # Different directories -> warning + prefer CUDA_PATH + with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): result = get_cuda_home_or_path() - assert pathlib.Path(result) == home + assert pathlib.Path(result) == path -def test_uses_cuda_path_if_home_missing(monkeypatch, tmp_path): +def test_uses_cuda_home_if_path_missing(monkeypatch, tmp_path): unset_env(monkeypatch) - only_path = tmp_path / "path" - only_path.mkdir() - monkeypatch.setenv("CUDA_PATH", str(only_path)) - assert pathlib.Path(get_cuda_home_or_path()) == only_path + only_home = tmp_path / "home" + only_home.mkdir() + monkeypatch.setenv("CUDA_HOME", str(only_home)) + assert pathlib.Path(get_cuda_home_or_path()) == only_home def test_no_warning_when_textually_equal_after_normalization(monkeypatch, tmp_path): @@ -68,8 +75,8 @@ def test_no_warning_when_textually_equal_after_normalization(monkeypatch, tmp_pa d.mkdir() with_slash = str(d) + ("/" if os.sep == "/" else "\\") - monkeypatch.setenv("CUDA_HOME", str(d)) - monkeypatch.setenv("CUDA_PATH", with_slash) + monkeypatch.setenv("CUDA_PATH", str(d)) + monkeypatch.setenv("CUDA_HOME", with_slash) # No warning; same logical directory with warnings.catch_warnings(record=True) as record: @@ -89,8 +96,8 @@ def test_no_warning_on_windows_case_only_difference(monkeypatch, tmp_path): upper = str(d).upper() lower = str(d).lower() - monkeypatch.setenv("CUDA_HOME", upper) - monkeypatch.setenv("CUDA_PATH", lower) + monkeypatch.setenv("CUDA_PATH", upper) + monkeypatch.setenv("CUDA_HOME", lower) with warnings.catch_warnings(record=True) as record: warnings.simplefilter("always") @@ -106,11 +113,11 @@ def test_warning_when_both_exist_and_are_different(monkeypatch, tmp_path): a.mkdir() b.mkdir() - monkeypatch.setenv("CUDA_HOME", str(a)) - monkeypatch.setenv("CUDA_PATH", str(b)) + monkeypatch.setenv("CUDA_PATH", str(a)) + monkeypatch.setenv("CUDA_HOME", str(b)) # Different actual dirs -> warning - with pytest.warns(UserWarning, match="Both CUDA_HOME and CUDA_PATH are set but differ"): + with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): result = get_cuda_home_or_path() assert pathlib.Path(result) == a @@ -124,10 +131,10 @@ def test_nonexistent_paths_fall_back_to_text_comparison(monkeypatch, tmp_path): a = tmp_path / "does_not_exist_a" b = tmp_path / "does_not_exist_b" - monkeypatch.setenv("CUDA_HOME", str(a)) - monkeypatch.setenv("CUDA_PATH", str(b)) + monkeypatch.setenv("CUDA_PATH", str(a)) + monkeypatch.setenv("CUDA_HOME", str(b)) - with pytest.warns(UserWarning, match="Both CUDA_HOME and CUDA_PATH are set but differ"): + with pytest.warns(UserWarning, match="Multiple CUDA environment variables are set but differ"): result = get_cuda_home_or_path() assert pathlib.Path(result) == a @@ -146,8 +153,8 @@ def test_samefile_equivalence_via_symlink_when_possible(monkeypatch, tmp_path): os.symlink(str(real_dir), str(link_dir), target_is_directory=True) # Set env vars to real and alias - monkeypatch.setenv("CUDA_HOME", str(real_dir)) - monkeypatch.setenv("CUDA_PATH", str(link_dir)) + monkeypatch.setenv("CUDA_PATH", str(real_dir)) + monkeypatch.setenv("CUDA_HOME", str(link_dir)) # Because they resolve to the same entry, no warning should be raised with warnings.catch_warnings(record=True) as record: @@ -157,6 +164,41 @@ def test_samefile_equivalence_via_symlink_when_possible(monkeypatch, tmp_path): assert len(record) == 0 +def test_cuda_env_vars_ordered_constant(): + """ + Verify the canonical search order constant is defined correctly. + CUDA_PATH must have higher priority than CUDA_HOME. + """ + assert CUDA_ENV_VARS_ORDERED == ("CUDA_PATH", "CUDA_HOME") + assert CUDA_ENV_VARS_ORDERED[0] == "CUDA_PATH" # highest priority + assert CUDA_ENV_VARS_ORDERED[1] == "CUDA_HOME" # lower priority + + +def test_search_order_matches_implementation(monkeypatch, tmp_path): + """ + Verify that get_cuda_home_or_path() follows the documented search order. + """ + unset_env(monkeypatch) + path_dir = tmp_path / "path_dir" + home_dir = tmp_path / "home_dir" + path_dir.mkdir() + home_dir.mkdir() + + # Set both env vars to different values + monkeypatch.setenv("CUDA_PATH", str(path_dir)) + monkeypatch.setenv("CUDA_HOME", str(home_dir)) + + # The result should match the first (highest priority) variable in CUDA_ENV_VARS_ORDERED + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + result = get_cuda_home_or_path() + + highest_priority_var = CUDA_ENV_VARS_ORDERED[0] + expected = os.environ.get(highest_priority_var) + assert result == expected + assert pathlib.Path(result) == path_dir # CUDA_PATH should win + + # --- unit tests for the helper itself (optional but nice to have) --- @@ -179,3 +221,36 @@ def test_paths_differ_samefile(tmp_path): # Should detect equivalence via samefile assert _paths_differ(str(real_dir), str(alias)) is False + + +def test_caching_behavior(monkeypatch, tmp_path): + """ + Verify that get_cuda_home_or_path() caches the result and returns the same + value even if environment variables change after the first call. + """ + unset_env(monkeypatch) + + first_dir = tmp_path / "first" + second_dir = tmp_path / "second" + first_dir.mkdir() + second_dir.mkdir() + + # Set initial value + monkeypatch.setenv("CUDA_PATH", str(first_dir)) + + # First call should return first_dir + result1 = get_cuda_home_or_path() + assert pathlib.Path(result1) == first_dir + + # Change the environment variable + monkeypatch.setenv("CUDA_PATH", str(second_dir)) + + # Second call should still return first_dir (cached value) + result2 = get_cuda_home_or_path() + assert pathlib.Path(result2) == first_dir + assert result1 == result2 + + # After clearing cache, should get new value + get_cuda_home_or_path.cache_clear() + result3 = get_cuda_home_or_path() + assert pathlib.Path(result3) == second_dir From 44b6050a0412b3ed2d42f8f70de2f10bcdd9f507 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Wed, 21 Jan 2026 17:53:46 -0800 Subject: [PATCH 02/13] restoring multiple path support --- cuda_bindings/setup.py | 7 +++++-- cuda_core/build_hooks.py | 19 +++++++++++------ cuda_core/tests/test_build_hooks.py | 32 ++++++++++++++++++++++++++--- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index 59080e39f9..977c969bd3 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -40,6 +40,8 @@ if not CUDA_HOME: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") +CUDA_HOME = CUDA_HOME.split(os.pathsep) + if os.environ.get("PARALLEL_LEVEL") is not None: warn( "Environment variable PARALLEL_LEVEL is deprecated. Use CUDA_PYTHON_PARALLEL_LEVEL instead", @@ -215,7 +217,7 @@ def parse_headers(header_dict): return found_types, found_functions, found_values, found_struct, struct_list -include_path_list = [os.path.join(CUDA_HOME, "include")] +include_path_list = [os.path.join(path, "include") for path in CUDA_HOME] header_dict = fetch_header_paths(required_headers, include_path_list) found_types, found_functions, found_values, found_struct, struct_list = parse_headers(header_dict) @@ -268,7 +270,8 @@ def generate_output(infile, local): ] + include_path_list library_dirs = [sysconfig.get_path("platlib"), os.path.join(os.sys.prefix, "lib")] cudalib_subdirs = [r"lib\x64"] if sys.platform == "win32" else ["lib64", "lib"] -library_dirs.extend(os.path.join(CUDA_HOME, subdir) for subdir in cudalib_subdirs) +for cuda_home in CUDA_HOME: + library_dirs.extend(os.path.join(cuda_home, subdir) for subdir in cudalib_subdirs) extra_compile_args = [] extra_cythonize_kwargs = {} diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 9fe039e2af..0211f89ab0 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -39,12 +39,18 @@ @functools.cache -def _get_cuda_path() -> str: +def _get_cuda_paths() -> list[str]: + """Get list of CUDA Toolkit paths from environment variables. + + Supports multiple paths separated by os.pathsep (: on Unix, ; on Windows). + Returns a list of paths for use in include_dirs and library_dirs. + """ CUDA_PATH = get_cuda_home_or_path() if not CUDA_PATH: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") - print("CUDA path:", CUDA_PATH) - return CUDA_PATH + CUDA_PATHS = CUDA_PATH.split(os.pathsep) + print("CUDA paths:", CUDA_PATHS) + return CUDA_PATHS @functools.cache @@ -69,8 +75,9 @@ def _determine_cuda_major_version() -> str: return cuda_major # Derive from the CUDA headers (the authoritative source for what we compile against). - cuda_path = _get_cuda_path() - cuda_h = os.path.join(cuda_path, "include", "cuda.h") + # Use the first path if multiple are specified + cuda_paths = _get_cuda_paths() + cuda_h = os.path.join(cuda_paths[0], "include", "cuda.h") try: with open(cuda_h, encoding="utf-8") as f: for line in f: @@ -124,7 +131,7 @@ def get_sources(mod_name): return sources - all_include_dirs = [os.path.join(_get_cuda_path(), "include")] + all_include_dirs = [os.path.join(path, "include") for path in _get_cuda_paths()] extra_compile_args = [] if COMPILE_FOR_COVERAGE: # CYTHON_TRACE_NOGIL indicates to trace nogil functions. It is not diff --git a/cuda_core/tests/test_build_hooks.py b/cuda_core/tests/test_build_hooks.py index e82ee67e84..a23150ddbf 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -65,7 +65,7 @@ def _check_version_detection( cuda_h = include_dir / "cuda.h" cuda_h.write_text(f"#define CUDA_VERSION {cuda_version}\n") - build_hooks._get_cuda_path.cache_clear() + build_hooks._get_cuda_paths.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() mock_env = { @@ -89,7 +89,7 @@ class TestGetCudaMajorVersion: @pytest.mark.parametrize("version", ["11", "12", "13", "14"]) def test_env_var_override(self, version): """CUDA_CORE_BUILD_MAJOR env var override works with various versions.""" - build_hooks._get_cuda_path.cache_clear() + build_hooks._get_cuda_paths.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() with mock.patch.dict(os.environ, {"CUDA_CORE_BUILD_MAJOR": version}, clear=False): result = build_hooks._determine_cuda_major_version() @@ -122,10 +122,36 @@ def test_env_var_takes_priority_over_headers(self): def test_missing_cuda_path_raises_error(self): """RuntimeError is raised when CUDA_PATH/CUDA_HOME not set and no env var override.""" - build_hooks._get_cuda_path.cache_clear() + build_hooks._get_cuda_paths.cache_clear() build_hooks._determine_cuda_major_version.cache_clear() with ( mock.patch.dict(os.environ, {}, clear=True), pytest.raises(RuntimeError, match="CUDA_PATH or CUDA_HOME"), ): build_hooks._determine_cuda_major_version() + + def test_multiple_cuda_paths(self): + """Multiple CUDA paths separated by os.pathsep are correctly handled.""" + with tempfile.TemporaryDirectory() as tmpdir1, tempfile.TemporaryDirectory() as tmpdir2: + # Create mock CUDA installations in both directories + for tmpdir in [tmpdir1, tmpdir2]: + include_dir = Path(tmpdir) / "include" + include_dir.mkdir() + cuda_h = include_dir / "cuda.h" + cuda_h.write_text("#define CUDA_VERSION 12080\n") + + build_hooks._get_cuda_paths.cache_clear() + build_hooks._determine_cuda_major_version.cache_clear() + + # Set CUDA_PATH with multiple paths + multiple_paths = os.pathsep.join([tmpdir1, tmpdir2]) + with mock.patch.dict(os.environ, {"CUDA_PATH": multiple_paths}, clear=True): + # Should return list of both paths + paths = build_hooks._get_cuda_paths() + assert len(paths) == 2 + assert paths[0] == tmpdir1 + assert paths[1] == tmpdir2 + + # Version detection should use first path + result = build_hooks._determine_cuda_major_version() + assert result == "12" From f222d8ae160ced1c06b398c93c4a35d5d9455742 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 09:40:36 -0800 Subject: [PATCH 03/13] reverting functionality that was rewritten for no reason --- cuda_core/build_hooks.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 0211f89ab0..824ebedbec 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -75,21 +75,21 @@ def _determine_cuda_major_version() -> str: return cuda_major # Derive from the CUDA headers (the authoritative source for what we compile against). - # Use the first path if multiple are specified - cuda_paths = _get_cuda_paths() - cuda_h = os.path.join(cuda_paths[0], "include", "cuda.h") - try: - with open(cuda_h, encoding="utf-8") as f: - for line in f: - m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line) - if m: - v = int(m.group(1)) - # CUDA_VERSION is e.g. 12020 for 12.2. - cuda_major = str(v // 1000) - print("CUDA MAJOR VERSION:", cuda_major) - return cuda_major - except OSError: - pass + cuda_path = _get_cuda_paths() + for root in cuda_path: + cuda_h = os.path.join(root, "include", "cuda.h") + try: + with open(cuda_h, encoding="utf-8") as f: + for line in f: + m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line) + if m: + v = int(m.group(1)) + # CUDA_VERSION is e.g. 12020 for 12.2. + cuda_major = str(v // 1000) + print("CUDA MAJOR VERSION:", cuda_major) + return cuda_major + except OSError: + continue # CUDA_PATH or CUDA_HOME is required for the build, so we should not reach here # in normal circumstances. Raise an error to make the issue clear. From 0e353628e07158540fc7d697aa12f0e4e9573507 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 11:28:53 -0800 Subject: [PATCH 04/13] wip --- cuda_bindings/setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index 977c969bd3..30b54f8a87 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -270,8 +270,7 @@ def generate_output(infile, local): ] + include_path_list library_dirs = [sysconfig.get_path("platlib"), os.path.join(os.sys.prefix, "lib")] cudalib_subdirs = [r"lib\x64"] if sys.platform == "win32" else ["lib64", "lib"] -for cuda_home in CUDA_HOME: - library_dirs.extend(os.path.join(cuda_home, subdir) for subdir in cudalib_subdirs) +library_dirs.extend(os.path.join(prefix, subdir) for prefix in CUDA_HOME for subdir in cudalib_subdirs) extra_compile_args = [] extra_cythonize_kwargs = {} From 5406b6db031fcf071ed6ac21c3c0a09f18330478 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 11:34:44 -0800 Subject: [PATCH 05/13] wip --- cuda_core/build_hooks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 824ebedbec..b41b9a0043 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -48,9 +48,9 @@ def _get_cuda_paths() -> list[str]: CUDA_PATH = get_cuda_home_or_path() if not CUDA_PATH: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") - CUDA_PATHS = CUDA_PATH.split(os.pathsep) - print("CUDA paths:", CUDA_PATHS) - return CUDA_PATHS + CUDA_PATH = CUDA_PATH.split(os.pathsep) + print("CUDA paths:", CUDA_PATH) + return CUDA_PATH @functools.cache From 41ed40ab7c0b60e5c1ed7cdff9849c41505edbd9 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 11:37:22 -0800 Subject: [PATCH 06/13] wip --- cuda_core/build_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index b41b9a0043..bbfefec4a8 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -131,7 +131,7 @@ def get_sources(mod_name): return sources - all_include_dirs = [os.path.join(path, "include") for path in _get_cuda_paths()] + all_include_dirs = list(os.path.join(root, "include") for root in _get_cuda_paths()) extra_compile_args = [] if COMPILE_FOR_COVERAGE: # CYTHON_TRACE_NOGIL indicates to trace nogil functions. It is not From 195a4725e5ccc3ad26fe23c06f30225ab3e90eb0 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 11:51:58 -0800 Subject: [PATCH 07/13] wip --- .../pathfinder/_utils/env_var_constants.py | 33 ------------------- .../cuda/pathfinder/_utils/env_vars.py | 21 +++++++++++- 2 files changed, 20 insertions(+), 34 deletions(-) delete mode 100644 cuda_pathfinder/cuda/pathfinder/_utils/env_var_constants.py diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_var_constants.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_var_constants.py deleted file mode 100644 index 38ed1fc3ef..0000000000 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_var_constants.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Constants for CUDA environment variable handling. - -This module defines the canonical search order for CUDA Toolkit environment variables -without any dependencies. It can be safely imported during bootstrap scenarios. - -Search Order Priority: - 1. CUDA_PATH (higher priority) - 2. CUDA_HOME (lower priority) - -.. versionadded:: 1.4.0 - Added centralized environment variable handling. - -.. versionchanged:: 1.4.0 - **Breaking Change**: Priority changed from CUDA_HOME > CUDA_PATH to CUDA_PATH > CUDA_HOME. -""" - -#: Canonical search order for CUDA Toolkit environment variables. -#: -#: This tuple defines the priority order used by :py:func:`~cuda.pathfinder._utils.env_vars.get_cuda_home_or_path` -#: and throughout cuda-python packages when determining which CUDA Toolkit to use. -#: -#: The first variable in the tuple has the highest priority. If multiple variables are set -#: and point to different locations, the first one is used and a warning is issued. -#: -#: .. note:: -#: **Breaking Change in v1.4.0**: The order changed from ``("CUDA_HOME", "CUDA_PATH")`` -#: to ``("CUDA_PATH", "CUDA_HOME")``, making ``CUDA_PATH`` the highest priority. -#: -#: :type: tuple[str, ...] -CUDA_ENV_VARS_ORDERED = ("CUDA_PATH", "CUDA_HOME") diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index f7dea9b9e5..8d3cbd3300 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -17,13 +17,32 @@ call determines the CUDA Toolkit path, and all subsequent calls return the cached value, even if environment variables change later. This ensures consistent behavior throughout the application lifecycle. + +.. versionadded:: 1.4.0 + Added centralized environment variable handling. + +.. versionchanged:: 1.4.0 + **Breaking Change**: Priority changed from CUDA_HOME > CUDA_PATH to CUDA_PATH > CUDA_HOME. """ import functools import os import warnings -from .env_var_constants import CUDA_ENV_VARS_ORDERED +#: Canonical search order for CUDA Toolkit environment variables. +#: +#: This tuple defines the priority order used by :py:func:`get_cuda_home_or_path` +#: and throughout cuda-python packages when determining which CUDA Toolkit to use. +#: +#: The first variable in the tuple has the highest priority. If multiple variables are set +#: and point to different locations, the first one is used and a warning is issued. +#: +#: .. note:: +#: **Breaking Change in v1.4.0**: The order changed from ``("CUDA_HOME", "CUDA_PATH")`` +#: to ``("CUDA_PATH", "CUDA_HOME")``, making ``CUDA_PATH`` the highest priority. +#: +#: :type: tuple[str, ...] +CUDA_ENV_VARS_ORDERED = ("CUDA_PATH", "CUDA_HOME") def _paths_differ(a: str, b: str) -> bool: From 6525f6cfac02cff7ec030e4b44ef06e78b148e0e Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 12:06:06 -0800 Subject: [PATCH 08/13] precommit edits --- conftest.py | 4 +--- cuda_bindings/docs/source/environment_variables.rst | 8 ++++---- cuda_core/build_hooks.py | 2 +- cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py | 12 ++++++------ cuda_pathfinder/docs/source/release/1.4.0-notes.rst | 6 +++--- cuda_pathfinder/tests/test_utils_env_vars.py | 12 ++++++------ 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/conftest.py b/conftest.py index 917a7ebc5b..2c7be61830 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -import os import pytest @@ -10,8 +9,7 @@ from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path except ImportError as e: raise ImportError( - "Failed to import cuda.pathfinder. " - "Please ensure cuda-pathfinder is installed: pip install cuda-pathfinder" + "Failed to import cuda.pathfinder. Please ensure cuda-pathfinder is installed: pip install cuda-pathfinder" ) from e diff --git a/cuda_bindings/docs/source/environment_variables.rst b/cuda_bindings/docs/source/environment_variables.rst index 8c79649989..b389f05b7a 100644 --- a/cuda_bindings/docs/source/environment_variables.rst +++ b/cuda_bindings/docs/source/environment_variables.rst @@ -17,17 +17,17 @@ Build-Time Environment Variables .. note:: **Breaking Change in v1.4.0**: The priority order changed from ``CUDA_HOME`` > ``CUDA_PATH`` to ``CUDA_PATH`` > ``CUDA_HOME``. - + **Migration Guide**: - + - If you only set one variable, no changes are needed - If you set both variables to the same location, no changes are needed - If you set both variables to different locations and relied on ``CUDA_HOME`` taking precedence, you should either: - + - Switch to using only ``CUDA_PATH`` (recommended) - Ensure both variables point to the same CUDA Toolkit installation - Be aware that ``CUDA_PATH`` will now be used - + A warning will be issued if both variables are set but point to different locations. - ``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_, where is derived from the cuda toolkit libraries used to build cuda-bindings. diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index bbfefec4a8..4bd77814d9 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -41,7 +41,7 @@ @functools.cache def _get_cuda_paths() -> list[str]: """Get list of CUDA Toolkit paths from environment variables. - + Supports multiple paths separated by os.pathsep (: on Unix, ; on Windows). Returns a list of paths for use in include_dirs and library_dirs. """ diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index 8d3cbd3300..afddd16825 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -102,15 +102,15 @@ def get_cuda_home_or_path() -> str | None: val = os.environ.get(var) if val is not None: set_vars[var] = val - + if not set_vars: return None - + # If multiple variables are set, check if they differ and warn if len(set_vars) > 1: # Check if any non-empty values actually differ non_empty_values = [(var, val) for var, val in set_vars.items() if val] - + if len(non_empty_values) > 1: # Check if any pair of non-empty values differs values_differ = False @@ -118,7 +118,7 @@ def get_cuda_home_or_path() -> str | None: if _paths_differ(non_empty_values[i][1], non_empty_values[i + 1][1]): values_differ = True break - + if values_differ: # Build a generic warning message that works for any number of variables var_list = "\n".join(f" {var}={val}" for var, val in set_vars.items()) @@ -130,10 +130,10 @@ def get_cuda_home_or_path() -> str | None: UserWarning, stacklevel=2, ) - + # Return the first (highest priority) set variable for var in CUDA_ENV_VARS_ORDERED: if var in set_vars: return set_vars[var] - + return None diff --git a/cuda_pathfinder/docs/source/release/1.4.0-notes.rst b/cuda_pathfinder/docs/source/release/1.4.0-notes.rst index 32d45aac03..bafed2209d 100644 --- a/cuda_pathfinder/docs/source/release/1.4.0-notes.rst +++ b/cuda_pathfinder/docs/source/release/1.4.0-notes.rst @@ -16,8 +16,8 @@ Breaking Changes * **CUDA environment variable priority changed**: ``CUDA_PATH`` now takes precedence over ``CUDA_HOME`` when both are set. Previously, ``CUDA_HOME`` had higher priority. If both variables are set and point to different locations, a warning will be issued and ``CUDA_PATH`` will be used. This change aligns with industry standards and NVIDIA's recommended practices. - **Migration Guide**: - + **Migration Guide**: + - If you rely on ``CUDA_HOME``, consider switching to ``CUDA_PATH`` - If you set both variables, ensure they point to the same CUDA Toolkit installation - If they differ intentionally, be aware that ``CUDA_PATH`` will now be used @@ -27,7 +27,7 @@ New Features ~~~~~~~~~~~~ * Added centralized CUDA environment variable handling with :py:func:`cuda.pathfinder._utils.env_vars.get_cuda_home_or_path()`. This function provides: - + - Consistent behavior across all cuda-python packages (cuda.pathfinder, cuda.core, cuda.bindings) - Intelligent path comparison that handles symlinks, case sensitivity, and trailing slashes - Result caching for performance (first call determines the path for the process lifetime) diff --git a/cuda_pathfinder/tests/test_utils_env_vars.py b/cuda_pathfinder/tests/test_utils_env_vars.py index 50351565c0..789aa868dd 100644 --- a/cuda_pathfinder/tests/test_utils_env_vars.py +++ b/cuda_pathfinder/tests/test_utils_env_vars.py @@ -229,27 +229,27 @@ def test_caching_behavior(monkeypatch, tmp_path): value even if environment variables change after the first call. """ unset_env(monkeypatch) - + first_dir = tmp_path / "first" second_dir = tmp_path / "second" first_dir.mkdir() second_dir.mkdir() - + # Set initial value monkeypatch.setenv("CUDA_PATH", str(first_dir)) - + # First call should return first_dir result1 = get_cuda_home_or_path() assert pathlib.Path(result1) == first_dir - + # Change the environment variable monkeypatch.setenv("CUDA_PATH", str(second_dir)) - + # Second call should still return first_dir (cached value) result2 = get_cuda_home_or_path() assert pathlib.Path(result2) == first_dir assert result1 == result2 - + # After clearing cache, should get new value get_cuda_home_or_path.cache_clear() result3 = get_cuda_home_or_path() From 3ca4b591ff3e073879a298287b00773f4e14d1a3 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 12:54:44 -0800 Subject: [PATCH 09/13] ci fixes --- cuda_bindings/pixi.toml | 4 ++-- cuda_bindings/pyproject.toml | 1 + cuda_bindings/setup.py | 10 +--------- cuda_core/build_hooks.py | 16 ++-------------- cuda_core/pixi.lock | 33 ++++++++++++++++++--------------- cuda_core/pixi.toml | 1 + cuda_core/pyproject.toml | 3 ++- 7 files changed, 27 insertions(+), 41 deletions(-) diff --git a/cuda_bindings/pixi.toml b/cuda_bindings/pixi.toml index 126cee7ceb..8c042429da 100644 --- a/cuda_bindings/pixi.toml +++ b/cuda_bindings/pixi.toml @@ -95,7 +95,7 @@ setuptools = ">=80" setuptools-scm = ">=8" cython = ">=3.2,<3.3" pyclibrary = ">=0.1.7" -cuda-pathfinder = ">=1.1,<2" +cuda-pathfinder = { path = "../cuda_pathfinder" } cuda-cudart-static = "*" cuda-nvrtc-dev = "*" cuda-profiler-api = "*" @@ -115,7 +115,7 @@ cuda-crt-dev_win-64 = "*" [package.run-dependencies] python = "*" -cuda-pathfinder = ">=1.1,<2" +cuda-pathfinder = { path = "../cuda_pathfinder" } libnvjitlink = "*" cuda-nvrtc = "*" cuda-nvvm = "*" diff --git a/cuda_bindings/pyproject.toml b/cuda_bindings/pyproject.toml index 614f7bb63a..fda45ce49a 100644 --- a/cuda_bindings/pyproject.toml +++ b/cuda_bindings/pyproject.toml @@ -6,6 +6,7 @@ requires = [ "setuptools_scm[simple]>=8", "cython>=3.2,<3.3", "pyclibrary>=0.1.7", + "cuda-pathfinder", ] build-backend = "setuptools.build_meta" diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index 30b54f8a87..7cb1f22d63 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -23,15 +23,7 @@ from setuptools.command.editable_wheel import _TopLevelFinder, editable_wheel from setuptools.extension import Extension -# Note: cuda_bindings requires cuda.pathfinder to be installed to ensure consistent -# environment variable handling across all CUDA Python packages. -try: - from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path -except ImportError as e: - raise RuntimeError( - "cuda.pathfinder package is required to build cuda_bindings. " - "Please install it first: pip install cuda-pathfinder" - ) from e +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path # ---------------------------------------------------------------------- # Fetch configuration options diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 4bd77814d9..63af8a2423 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -16,20 +16,6 @@ from setuptools import Extension from setuptools import build_meta as _build_meta -# Import centralized CUDA environment variable handling -# Note: This import may fail at build-dependency-resolution time if cuda-pathfinder -# is not yet installed, but it's guaranteed to be available when _get_cuda_path() -# is actually called (during wheel build time). -try: - from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path -except ImportError as e: - raise ImportError( - "Failed to import cuda.pathfinder. " - "Please ensure cuda-pathfinder is installed as a build dependency. " - "If building cuda-core, cuda-pathfinder should be automatically installed. " - "If this error persists, try: pip install cuda-pathfinder" - ) from e - prepare_metadata_for_build_editable = _build_meta.prepare_metadata_for_build_editable prepare_metadata_for_build_wheel = _build_meta.prepare_metadata_for_build_wheel build_sdist = _build_meta.build_sdist @@ -45,6 +31,8 @@ def _get_cuda_paths() -> list[str]: Supports multiple paths separated by os.pathsep (: on Unix, ; on Windows). Returns a list of paths for use in include_dirs and library_dirs. """ + from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + CUDA_PATH = get_cuda_home_or_path() if not CUDA_PATH: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") diff --git a/cuda_core/pixi.lock b/cuda_core/pixi.lock index b100dd71d2..e4c85d3624 100644 --- a/cuda_core/pixi.lock +++ b/cuda_core/pixi.lock @@ -280,7 +280,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 cu13: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -536,7 +536,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 default: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -792,7 +792,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - conda: . - build: py314h625260f_0 + build: py314hae7e39d_0 packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -1184,36 +1184,39 @@ packages: - conda: . name: cuda-core version: 0.5.0 - build: py314h625260f_0 - subdir: win-64 + build: py314ha479ada_0 + subdir: linux-aarch64 variants: python: 3.14.* - target_platform: win-64 + target_platform: linux-aarch64 depends: - python - numpy - cuda-bindings - - vc >=14.1,<15 - - vc14_runtime >=14.16.27033 + - libgcc >=15 + - libgcc >=15 + - libstdcxx >=15 - python_abi 3.14.* *_cp314 + - cuda-cudart >=13.1.80,<14.0a0 license: Apache-2.0 - conda: . name: cuda-core version: 0.5.0 - build: py314ha479ada_0 - subdir: linux-aarch64 + build: py314hae7e39d_0 + subdir: win-64 variants: + c_compiler: vs2022 + cxx_compiler: vs2022 python: 3.14.* - target_platform: linux-aarch64 + target_platform: win-64 depends: - python - numpy - cuda-bindings - - libgcc >=15 - - libgcc >=15 - - libstdcxx >=15 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 - python_abi 3.14.* *_cp314 - - cuda-cudart >=13.1.80,<14.0a0 license: Apache-2.0 - conda: https://conda.anaconda.org/conda-forge/noarch/cuda-crt-dev_linux-64-12.9.86-ha770c72_2.conda sha256: e6257534c4b4b6b8a1192f84191c34906ab9968c92680fa09f639e7846a87304 diff --git a/cuda_core/pixi.toml b/cuda_core/pixi.toml index 5ee25e1e5d..7613b07d8f 100644 --- a/cuda_core/pixi.toml +++ b/cuda_core/pixi.toml @@ -101,6 +101,7 @@ setuptools-scm = ">=8" cython = ">=3.2,<3.3" cuda-cudart-dev = "*" cuda-profiler-api = "*" +cuda-pathfinder = { path = "../cuda_pathfinder" } # this doesn't work because Cython cannot find editable-installed build-time # dependencies https://github.com/cython/cython/issues/7326 # cuda-bindings = { path = "../cuda_bindings" } diff --git a/cuda_core/pyproject.toml b/cuda_core/pyproject.toml index f775ac8813..f29c9c44a8 100644 --- a/cuda_core/pyproject.toml +++ b/cuda_core/pyproject.toml @@ -6,7 +6,8 @@ requires = [ "setuptools>=80", "setuptools-scm[simple]>=8", - "Cython>=3.2,<3.3" + "Cython>=3.2,<3.3", + "cuda-pathfinder" ] build-backend = "build_hooks" backend-path = ["."] From 2921b31071924e1b4b3ed793f61672c613c0fcc7 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 12:55:48 -0800 Subject: [PATCH 10/13] wip --- cuda_bindings/setup.py | 3 +-- cuda_core/build_hooks.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index 7cb1f22d63..44a6fbdd85 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -13,6 +13,7 @@ import tempfile from warnings import warn +from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path from Cython import Tempita from Cython.Build import cythonize from pyclibrary import CParser @@ -23,8 +24,6 @@ from setuptools.command.editable_wheel import _TopLevelFinder, editable_wheel from setuptools.extension import Extension -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path - # ---------------------------------------------------------------------- # Fetch configuration options diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 63af8a2423..2ee75271fc 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -32,7 +32,7 @@ def _get_cuda_paths() -> list[str]: Returns a list of paths for use in include_dirs and library_dirs. """ from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path - + CUDA_PATH = get_cuda_home_or_path() if not CUDA_PATH: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") From b127a4707f747f36871601287cbcbd7bd2b7e749 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 13:04:31 -0800 Subject: [PATCH 11/13] wip --- cuda_bindings/setup.py | 9 +++++++-- cuda_core/build_hooks.py | 10 +++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index 44a6fbdd85..d5bc478cd5 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -13,7 +13,6 @@ import tempfile from warnings import warn -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path from Cython import Tempita from Cython.Build import cythonize from pyclibrary import CParser @@ -27,7 +26,13 @@ # ---------------------------------------------------------------------- # Fetch configuration options -CUDA_HOME = get_cuda_home_or_path() +try: + from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + CUDA_HOME = get_cuda_home_or_path() +except ImportError: + # Fallback for build environments where cuda-pathfinder may not be available + CUDA_HOME = os.environ.get("CUDA_HOME", os.environ.get("CUDA_PATH", None)) + if not CUDA_HOME: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 2ee75271fc..521fc984a7 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -31,9 +31,13 @@ def _get_cuda_paths() -> list[str]: Supports multiple paths separated by os.pathsep (: on Unix, ; on Windows). Returns a list of paths for use in include_dirs and library_dirs. """ - from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path - - CUDA_PATH = get_cuda_home_or_path() + try: + from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + CUDA_PATH = get_cuda_home_or_path() + except ImportError: + # Fallback for build environments where cuda-pathfinder may not be available + CUDA_PATH = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME", None)) + if not CUDA_PATH: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") CUDA_PATH = CUDA_PATH.split(os.pathsep) From 741f6c61413816cda024bbb1eb7e9117b0854bb3 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 13:05:00 -0800 Subject: [PATCH 12/13] wip --- cuda_bindings/setup.py | 1 + cuda_core/build_hooks.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cuda_bindings/setup.py b/cuda_bindings/setup.py index d5bc478cd5..dfa5e0abd6 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -28,6 +28,7 @@ try: from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + CUDA_HOME = get_cuda_home_or_path() except ImportError: # Fallback for build environments where cuda-pathfinder may not be available diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 521fc984a7..13d8dc3e63 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -33,11 +33,12 @@ def _get_cuda_paths() -> list[str]: """ try: from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path + CUDA_PATH = get_cuda_home_or_path() except ImportError: # Fallback for build environments where cuda-pathfinder may not be available CUDA_PATH = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME", None)) - + if not CUDA_PATH: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") CUDA_PATH = CUDA_PATH.split(os.pathsep) From 2ce673ec95aee5f358aced18269bd0d90c18d499 Mon Sep 17 00:00:00 2001 From: Rob Parolin Date: Thu, 22 Jan 2026 14:45:16 -0800 Subject: [PATCH 13/13] trying to fix ci build-hooks failures --- cuda_bindings/docs/source/install.rst | 4 ---- cuda_bindings/pixi.lock | 6 +++--- cuda_bindings/pixi.toml | 4 ++-- cuda_core/build_hooks.py | 14 +------------- cuda_core/examples/thread_block_cluster.py | 4 +--- cuda_core/pixi.toml | 2 +- cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py | 6 ------ 7 files changed, 8 insertions(+), 32 deletions(-) diff --git a/cuda_bindings/docs/source/install.rst b/cuda_bindings/docs/source/install.rst index 26d527cd02..d647360ff2 100644 --- a/cuda_bindings/docs/source/install.rst +++ b/cuda_bindings/docs/source/install.rst @@ -93,10 +93,6 @@ Source builds require that the provided CUDA headers are of the same major.minor $ export CUDA_PATH=/usr/local/cuda -.. note:: - - The CUDA Toolkit path is determined once at the start of the build process and cached. If you need to change the path during development, restart your build environment. - See `Environment Variables `_ for a description of other build-time environment variables. .. note:: diff --git a/cuda_bindings/pixi.lock b/cuda_bindings/pixi.lock index 3a9ae219de..17184a9ea0 100644 --- a/cuda_bindings/pixi.lock +++ b/cuda_bindings/pixi.lock @@ -1458,7 +1458,7 @@ packages: target_platform: linux-64 depends: - python - - cuda-pathfinder >=1.1,<2 + - cuda-pathfinder - libnvjitlink - cuda-nvrtc - cuda-nvrtc >=13.1.115,<14.0a0 @@ -1480,7 +1480,7 @@ packages: target_platform: linux-aarch64 depends: - python - - cuda-pathfinder >=1.1,<2 + - cuda-pathfinder - libnvjitlink - cuda-nvrtc - cuda-nvrtc >=13.1.115,<14.0a0 @@ -1504,7 +1504,7 @@ packages: target_platform: win-64 depends: - python - - cuda-pathfinder >=1.1,<2 + - cuda-pathfinder - libnvjitlink - cuda-nvrtc - cuda-nvrtc >=13.1.115,<14.0a0 diff --git a/cuda_bindings/pixi.toml b/cuda_bindings/pixi.toml index 8c042429da..659f470293 100644 --- a/cuda_bindings/pixi.toml +++ b/cuda_bindings/pixi.toml @@ -95,7 +95,7 @@ setuptools = ">=80" setuptools-scm = ">=8" cython = ">=3.2,<3.3" pyclibrary = ">=0.1.7" -cuda-pathfinder = { path = "../cuda_pathfinder" } +cuda-pathfinder = "*" cuda-cudart-static = "*" cuda-nvrtc-dev = "*" cuda-profiler-api = "*" @@ -115,7 +115,7 @@ cuda-crt-dev_win-64 = "*" [package.run-dependencies] python = "*" -cuda-pathfinder = { path = "../cuda_pathfinder" } +cuda-pathfinder = "*" libnvjitlink = "*" cuda-nvrtc = "*" cuda-nvvm = "*" diff --git a/cuda_core/build_hooks.py b/cuda_core/build_hooks.py index 13d8dc3e63..bb7951db62 100644 --- a/cuda_core/build_hooks.py +++ b/cuda_core/build_hooks.py @@ -26,19 +26,7 @@ @functools.cache def _get_cuda_paths() -> list[str]: - """Get list of CUDA Toolkit paths from environment variables. - - Supports multiple paths separated by os.pathsep (: on Unix, ; on Windows). - Returns a list of paths for use in include_dirs and library_dirs. - """ - try: - from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path - - CUDA_PATH = get_cuda_home_or_path() - except ImportError: - # Fallback for build environments where cuda-pathfinder may not be available - CUDA_PATH = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME", None)) - + CUDA_PATH = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME", None)) if not CUDA_PATH: raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") CUDA_PATH = CUDA_PATH.split(os.pathsep) diff --git a/cuda_core/examples/thread_block_cluster.py b/cuda_core/examples/thread_block_cluster.py index dbc04d2289..f1ea8b8579 100644 --- a/cuda_core/examples/thread_block_cluster.py +++ b/cuda_core/examples/thread_block_cluster.py @@ -27,9 +27,7 @@ sys.exit(0) # prepare include -from cuda.pathfinder._utils.env_vars import get_cuda_home_or_path - -cuda_path = get_cuda_home_or_path() +cuda_path = os.environ.get("CUDA_PATH", os.environ.get("CUDA_HOME")) if cuda_path is None: print("this demo requires a valid CUDA_PATH environment variable set", file=sys.stderr) sys.exit(0) diff --git a/cuda_core/pixi.toml b/cuda_core/pixi.toml index 7613b07d8f..052914303c 100644 --- a/cuda_core/pixi.toml +++ b/cuda_core/pixi.toml @@ -101,7 +101,7 @@ setuptools-scm = ">=8" cython = ">=3.2,<3.3" cuda-cudart-dev = "*" cuda-profiler-api = "*" -cuda-pathfinder = { path = "../cuda_pathfinder" } +cuda-pathfinder = "*" # this doesn't work because Cython cannot find editable-installed build-time # dependencies https://github.com/cython/cython/issues/7326 # cuda-bindings = { path = "../cuda_bindings" } diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index afddd16825..710fcafd38 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -17,12 +17,6 @@ call determines the CUDA Toolkit path, and all subsequent calls return the cached value, even if environment variables change later. This ensures consistent behavior throughout the application lifecycle. - -.. versionadded:: 1.4.0 - Added centralized environment variable handling. - -.. versionchanged:: 1.4.0 - **Breaking Change**: Priority changed from CUDA_HOME > CUDA_PATH to CUDA_PATH > CUDA_HOME. """ import functools