diff --git a/conftest.py b/conftest.py index 7c4cf3a761..2c7be61830 100644 --- a/conftest.py +++ b/conftest.py @@ -1,13 +1,20 @@ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -import os 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..b389f05b7a 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..d647360ff2 100644 --- a/cuda_bindings/docs/source/install.rst +++ b/cuda_bindings/docs/source/install.rst @@ -87,11 +87,11 @@ 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 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..17184a9ea0 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 @@ -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 @@ -1473,42 +1473,45 @@ 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 + - cuda-pathfinder - libnvjitlink - 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 + - cuda-pathfinder - libnvjitlink - 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..659f470293 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 = "*" cuda-cudart-static = "*" cuda-nvrtc-dev = "*" cuda-profiler-api = "*" @@ -114,7 +115,7 @@ cuda-crt-dev_win-64 = "*" [package.run-dependencies] python = "*" -cuda-pathfinder = ">=1.1,<2" +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 bfa1ae7826..dfa5e0abd6 100644 --- a/cuda_bindings/setup.py +++ b/cuda_bindings/setup.py @@ -26,9 +26,16 @@ # ---------------------------------------------------------------------- # Fetch configuration options -CUDA_HOME = os.environ.get("CUDA_HOME", os.environ.get("CUDA_PATH", None)) +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_HOME or CUDA_PATH is not set") + raise RuntimeError("Environment variable CUDA_PATH or CUDA_HOME is not set") CUDA_HOME = CUDA_HOME.split(os.pathsep) 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/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..052914303c 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 = "*" # 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 = ["."] 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..a23150ddbf 100644 --- a/cuda_core/tests/test_build_hooks.py +++ b/cuda_core/tests/test_build_hooks.py @@ -129,3 +129,29 @@ def test_missing_cuda_path_raises_error(self): 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" 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_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index cf78a627cb..710fcafd38 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -1,9 +1,43 @@ # 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 +#: 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: """ @@ -32,20 +66,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..bafed2209d --- /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..789aa868dd 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