Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import functools
import os
import shutil

from cuda.pathfinder._binaries import supported_nvidia_binaries
from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES
from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS
Expand All @@ -28,6 +28,51 @@ def _normalize_utility_name(utility_name: str) -> str:
return utility_name


def _is_executable_file(path: str) -> bool:
"""Return True if ``path`` is a file the OS would run as an executable.

On Windows executability is determined by the file extension (the
candidate name already carries one), so existence is sufficient. On POSIX
the execute permission bit must be set.
"""
if not os.path.isfile(path):
return False
if IS_WINDOWS:
return True
return os.access(path, os.X_OK)


def _ctk_bin_subdirs(root: str) -> list[str]:
if IS_WINDOWS:
return [
os.path.join(root, "bin", "x64"),
os.path.join(root, "bin", "x86_64"),
os.path.join(root, "bin"),
]
return [os.path.join(root, "bin")]


def _resolve_ctk_root_via_canary() -> str | None:
from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import resolve_ctk_root_via_canary

ctk_root: str | None = resolve_ctk_root_via_canary(CTK_ROOT_CANARY_ANCHOR_LIBNAMES[0])
return ctk_root


def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[str]) -> str | None:
"""Resolve ``normalized_name`` against ``dirs`` in order."""
seen: set[str] = set()
for directory in dirs:
if directory in seen:
continue
assert directory
seen.add(directory)
candidate = os.path.join(directory, normalized_name)
if _is_executable_file(candidate):
return candidate
return None


@functools.cache
def find_nvidia_binary_utility(utility_name: str) -> str | None:
"""Locate a CUDA binary utility executable.
Expand Down Expand Up @@ -65,6 +110,12 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows,
or just ``bin`` on Linux.

4. **CTK-root canary fallback**

- Only when steps 1-3 miss: resolve the ``cudart`` library through the
OS dynamic loader, derive the CUDA Toolkit root from it, and search
that root's bin layout.

Note:
Results are cached using ``@functools.cache`` for performance. The cache
persists for the lifetime of the process.
Expand All @@ -73,6 +124,9 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
(``.exe``, ``.bat``, ``.cmd``). On Unix-like systems, executables
are identified by the ``X_OK`` (execute) permission bit.

Lookup is restricted to the trusted directories and the canary-derived
CTK root listed above.

Example:
>>> from cuda.pathfinder import find_nvidia_binary_utility
>>> nvdisasm = find_nvidia_binary_utility("nvdisasm")
Expand All @@ -98,10 +152,15 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:

# 3. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH)
if (cuda_home := get_cuda_path_or_home()) is not None:
if IS_WINDOWS:
dirs.append(os.path.join(cuda_home, "bin", "x64"))
dirs.append(os.path.join(cuda_home, "bin", "x86_64"))
dirs.append(os.path.join(cuda_home, "bin"))
dirs.extend(_ctk_bin_subdirs(cuda_home))

normalized_name = _normalize_utility_name(utility_name)
return shutil.which(normalized_name, path=os.pathsep.join(dirs))
found = _resolve_in_trusted_dirs(normalized_name, dirs)
if found is not None:
return found

# 4. CTK-root canary fallback.
ctk_root = _resolve_ctk_root_via_canary()
if ctk_root is not None:
return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(ctk_root))
return None
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from dataclasses import dataclass
from typing import Literal

from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES

PackagedWith = Literal["ctk", "other", "driver"]


Expand Down Expand Up @@ -73,7 +75,7 @@ class DescriptorSpec:
site_packages_windows=("nvidia/cu13/bin/x86_64", "nvidia/cuda_nvcc/nvvm/bin"),
anchor_rel_dirs_linux=("nvvm/lib64",),
anchor_rel_dirs_windows=("nvvm/bin/*", "nvvm/bin"),
ctk_root_canary_anchor_libnames=("cudart",),
ctk_root_canary_anchor_libnames=CTK_ROOT_CANARY_ANCHOR_LIBNAMES,
),
DescriptorSpec(
name="cublas",
Expand Down Expand Up @@ -291,7 +293,7 @@ class DescriptorSpec:
site_packages_windows=("nvidia/cu13/bin/x86_64", "nvidia/cuda_cupti/bin"),
anchor_rel_dirs_linux=("extras/CUPTI/lib64", "lib"),
anchor_rel_dirs_windows=("extras/CUPTI/lib64", "bin"),
ctk_root_canary_anchor_libnames=("cudart",),
ctk_root_canary_anchor_libnames=CTK_ROOT_CANARY_ANCHOR_LIBNAMES,
),
DescriptorSpec(
name="cudla",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,26 @@ def _loadable_via_canary_subprocess(libname: str, *, timeout: float = _CANARY_PR
return _resolve_system_loaded_abs_path_in_subprocess(libname, timeout=timeout) is not None


def resolve_ctk_root_via_canary(canary_libname: str) -> str | None:
"""Resolve the CUDA Toolkit root from a system-loadable canary library.

The canary library's absolute path is resolved by the OS dynamic loader in
an isolated subprocess, which honors ``LD_LIBRARY_PATH`` on Linux and the
native DLL search on Windows. The toolkit root is then derived from that
path. Returns ``None`` if the canary cannot be resolved or no root can be
derived. The ambient ``PATH`` is never consulted.
"""
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(canary_libname)
if canary_abs_path is None:
return None
ctk_root: str | None = derive_ctk_root(canary_abs_path)
return ctk_root


def _try_ctk_root_canary(ctx: SearchContext) -> str | None:
"""Try CTK-root canary fallback for descriptor-configured libraries."""
for canary_libname in ctx.desc.ctk_root_canary_anchor_libnames:
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(canary_libname)
if canary_abs_path is None:
continue
ctk_root = derive_ctk_root(canary_abs_path)
ctk_root = resolve_ctk_root_via_canary(canary_libname)
if ctk_root is None:
continue
find = find_via_ctk_root(ctx, ctk_root)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
platform_include_subdirs,
resolve_conda_anchor,
)
from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES
from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages

Expand Down Expand Up @@ -121,7 +122,7 @@ def find_via_ctk_root_canary(desc: HeaderDescriptor) -> LocatedHeaderDir | None:
"""
if not desc.use_ctk_root_canary:
return None
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess("cudart")
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(CTK_ROOT_CANARY_ANCHOR_LIBNAMES[0])
if canary_abs_path is None:
return None
ctk_root = derive_ctk_root(canary_abs_path)
Expand Down
4 changes: 4 additions & 0 deletions cuda_pathfinder/cuda/pathfinder/_utils/ctk_root_canary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

CTK_ROOT_CANARY_ANCHOR_LIBNAMES = ("cudart",)
18 changes: 18 additions & 0 deletions cuda_pathfinder/docs/source/release/1.6.0-notes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0
.. py:currentmodule:: cuda.pathfinder
``cuda-pathfinder`` 1.6.0 Release notes
=======================================

Highlights
----------

* :func:`find_nvidia_binary_utility` now resolves binaries through a bounded,
deterministic search of trusted directories instead of ``shutil.which`` and
gains a CTK-root canary fallback. When the NVIDIA wheel, ``CONDA_PREFIX``, and
``CUDA_HOME`` / ``CUDA_PATH`` directories all miss, ``cudart`` is resolved
through the OS dynamic loader, the CUDA Toolkit root is derived from that
path, and its ``bin`` layout is searched.
(`PR #2196 <https://github.com/NVIDIA/cuda-python/pull/2196>`_)
Comment on lines +12 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please combine this into one bullet point? I think it's best to also delete the "This locates the utility for users who follow..." sentence here.

30 changes: 30 additions & 0 deletions cuda_pathfinder/tests/test_ctk_root_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
_load_lib_no_cache,
_resolve_system_loaded_abs_path_in_subprocess,
_try_ctk_root_canary,
resolve_ctk_root_via_canary,
)
from cuda.pathfinder._dynamic_libs.search_steps import (
SearchContext,
Expand Down Expand Up @@ -369,6 +370,35 @@ def test_canary_skips_when_abs_path_none(mocker):
assert _try_ctk_root_canary(_ctx("nvvm")) is None


# ---------------------------------------------------------------------------
# resolve_ctk_root_via_canary (shared by lib and binary discovery)
# ---------------------------------------------------------------------------


def test_resolve_ctk_root_via_canary_returns_root(tmp_path, mocker):
ctk_root = tmp_path / "cuda-13"
_create_cudart_in_ctk(ctk_root)
probe = mocker.patch(
f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess",
return_value=_fake_canary_path(ctk_root),
)
assert resolve_ctk_root_via_canary("cudart") == str(ctk_root)
probe.assert_called_once_with("cudart")


def test_resolve_ctk_root_via_canary_none_when_probe_fails(mocker):
mocker.patch(f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess", return_value=None)
assert resolve_ctk_root_via_canary("cudart") is None


def test_resolve_ctk_root_via_canary_none_when_unrecognized(mocker):
mocker.patch(
f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess",
return_value=os.path.join(os.sep, "weird", "path", "libcudart.so.13"),
)
assert resolve_ctk_root_via_canary("cudart") is None


# ---------------------------------------------------------------------------
# _load_lib_no_cache search-order
# ---------------------------------------------------------------------------
Expand Down
Loading