From 408e5871b9c284d90130f1f59048e935de3fb1b2 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Sun, 9 Aug 2026 23:47:29 -0700 Subject: [PATCH 01/11] Fix Windows binary utility discovery on Arm64 --- .../_binaries/find_nvidia_binary_utility.py | 142 +++++++++++++-- .../_binaries/supported_nvidia_binaries.py | 6 + .../cuda/pathfinder/_utils/windows_arch.py | 15 ++ .../tests/test_find_nvidia_binaries.py | 161 ++++++++++++++++++ cuda_pathfinder/tests/test_search_steps.py | 19 +++ 5 files changed, 333 insertions(+), 10 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 834db8fe8fa..72fc049b64f 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -2,13 +2,29 @@ # SPDX-License-Identifier: Apache-2.0 import functools +import importlib import os +from collections.abc import Iterable +from typing import Any 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 +from cuda.pathfinder._utils.windows_arch import windows_machine_arch + +_NSIGHT_REGISTRY_ROOT = r"SOFTWARE\NVIDIA Corporation\Installed Products\Nsight" + +_NSYS_TARGET_DIR_BY_ARCH = { + "x64": "target-windows-x64", + "arm64": "target-windows-armv8", +} + +_NCU_TARGET_DIR_BY_ARCH = { + "x64": os.path.join("target", "windows-desktop-win7-x64"), + "arm64": os.path.join("target", "windows-desktop-win10-t23x-a64"), +} class UnsupportedBinaryError(Exception): @@ -46,6 +62,71 @@ def _ctk_bin_subdirs(root: str) -> list[str]: return [os.path.join(root, "bin")] +def _resolve_candidate_paths(candidates: Iterable[str]) -> str | None: + """Return the first executable candidate, preserving candidate order.""" + seen: set[str] = set() + for candidate in candidates: + if candidate in seen: + continue + seen.add(candidate) + if _is_executable_candidate(candidate): + return os.path.abspath(candidate) + return None + + +def _find_windows_compute_sanitizer(ctk_root: str) -> str | None: + return _resolve_candidate_paths( + ( + os.path.join(ctk_root, "bin", "compute-sanitizer.bat"), + os.path.join(ctk_root, "compute-sanitizer", "compute-sanitizer.exe"), + ) + ) + + +def _windows_installed_nsight_root(product: str) -> str | None: + """Return the active Nsight product installation recorded by its MSI.""" + # ``winreg`` attributes are absent from the type stubs on non-Windows hosts. + winreg: Any = importlib.import_module("winreg") + + access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY + product_key_path = rf"{_NSIGHT_REGISTRY_ROOT}\{product}" + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, product_key_path, 0, access) as product_key: + current_version, _ = winreg.QueryValueEx(product_key, "CurrentVersion") + if not isinstance(current_version, str) or not current_version: + raise RuntimeError(f"Invalid CurrentVersion value in {product_key_path!r}") + with winreg.OpenKey(product_key, current_version, 0, access) as version_key: + install_root, _ = winreg.QueryValueEx(version_key, None) + except FileNotFoundError: + return None + + if not isinstance(install_root, str) or not install_root: + raise RuntimeError(f"Invalid installation directory for {product_key_path!r} version {current_version!r}") + return install_root + + +def _find_windows_nsys() -> str | None: + install_root = _windows_installed_nsight_root("Systems") + if install_root is None: + return None + + target_dir = _NSYS_TARGET_DIR_BY_ARCH[windows_machine_arch()] + return _resolve_candidate_paths((os.path.join(install_root, target_dir, "nsys.exe"),)) + + +def _find_windows_ncu() -> str | None: + install_root = _windows_installed_nsight_root("Compute") + if install_root is None: + return None + + launcher = os.path.join(install_root, "ncu.bat") + if (found := _resolve_candidate_paths((launcher,))) is not None: + return found + + target_dir = _NCU_TARGET_DIR_BY_ARCH[windows_machine_arch()] + return _resolve_candidate_paths((os.path.join(install_root, target_dir, "ncu.exe"),)) + + def _resolve_ctk_root_via_canary() -> str | None: from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import resolve_ctk_root_via_canary @@ -69,6 +150,20 @@ def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[str]) -> str | Non return None +def _resolve_names_in_trusted_dirs(candidate_names: tuple[str, ...], dirs: list[str]) -> str | None: + """Resolve ordered candidate names within each trusted directory.""" + seen: set[str] = set() + for directory in dirs: + if directory in seen: + continue + assert directory + seen.add(directory) + found = _resolve_candidate_paths(os.path.join(directory, name) for name in candidate_names) + if found is not None: + return found + return None + + @functools.cache def find_nvidia_binary_utility(utility_name: str) -> str | None: """Locate a CUDA binary utility executable. @@ -100,15 +195,21 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: environment variable, which use platform-specific bin directory layouts (``Library/bin`` on Windows, ``bin`` on Linux). - 3. **CUDA Toolkit environment variables** + 3. **Windows Nsight installations** + + - On Windows, locate Nsight Systems and Nsight Compute from their + installer registry entries. Select architecture-specific binaries + using the native machine architecture, independent of Python. + + 4. **CUDA Toolkit environment variables** - Use ``CUDA_HOME`` or ``CUDA_PATH`` (in that order), searching ``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows, or just ``bin`` on Linux. - 4. **CTK-root canary fallback** + 5. **CTK-root canary fallback** - - Only when steps 1-3 miss: resolve the ``cudart`` library through the + - Only when steps 1-4 miss: resolve the ``cudart`` library through the OS dynamic loader, derive the CUDA Toolkit root from it, and search that root's bin layout. @@ -132,6 +233,10 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: if utility_name not in supported_nvidia_binaries.SUPPORTED_BINARIES: raise UnsupportedBinaryError(utility_name) + resolved_name = ( + supported_nvidia_binaries.WINDOWS_BINARY_ALIASES.get(utility_name, utility_name) if IS_WINDOWS else utility_name + ) + # 1. Search in site-packages (NVIDIA wheels) candidate_dirs = supported_nvidia_binaries.SITE_PACKAGES_BINDIRS.get(utility_name, ()) dirs = [] @@ -146,17 +251,34 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: else: dirs.append(os.path.join(conda_prefix, "bin")) - # 3. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH) - if (cuda_home := get_cuda_path_or_home()) is not None: - dirs.extend(_ctk_bin_subdirs(cuda_home)) - - normalized_name = _normalize_utility_name(utility_name) - found = _resolve_in_trusted_dirs(normalized_name, dirs) + normalized_name = _normalize_utility_name(resolved_name) + if IS_WINDOWS and resolved_name in ("compute-sanitizer", "ncu"): + candidate_names = (f"{resolved_name}.bat", normalized_name) + found = _resolve_names_in_trusted_dirs(candidate_names, dirs) + else: + found = _resolve_in_trusted_dirs(normalized_name, dirs) if found is not None: return found - # 4. CTK-root canary fallback. + # 3. Nsight tools are separate products and are not installed under CTK. + if IS_WINDOWS and resolved_name == "nsys": + return _find_windows_nsys() + if IS_WINDOWS and resolved_name == "ncu": + return _find_windows_ncu() + + # 4. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH). + if (cuda_home := get_cuda_path_or_home()) is not None: + if IS_WINDOWS and resolved_name == "compute-sanitizer": + found = _find_windows_compute_sanitizer(cuda_home) + else: + found = _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(cuda_home)) + if found is not None: + return found + + # 5. CTK-root canary fallback. ctk_root = _resolve_ctk_root_via_canary() if ctk_root is not None: + if IS_WINDOWS and resolved_name == "compute-sanitizer": + return _find_windows_compute_sanitizer(ctk_root) return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(ctk_root)) return None diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py b/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py index ac70378f112..b5fb7428d6c 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py @@ -31,4 +31,10 @@ "nsight-compute": (_NSIGHT_COMPUTE_BIN,), } +# Names accepted by the public API that differ from the installed executable. +WINDOWS_BINARY_ALIASES = { + "nsight-sys": "nsys", + "nsight-compute": "ncu", +} + SUPPORTED_BINARIES_ALL = SUPPORTED_BINARIES = tuple(SITE_PACKAGES_BINDIRS.keys()) diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py b/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py index 9313f3a9f17..d7b6264d16b 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py @@ -3,6 +3,7 @@ from __future__ import annotations +import platform import sysconfig WINDOWS_PE_MACHINE_BY_ARCH = { @@ -35,6 +36,20 @@ def windows_python_arch() -> str: raise UnsupportedArchError(raw_platform_tag) +def windows_machine_arch() -> str: + """Return the native Windows machine architecture.""" + raw_machine = platform.machine() + machine = raw_machine.lower().replace("_", "-") + + if machine in ("amd64", "x86-64"): + return "x64" + + if machine in ("arm64", "aarch64"): + return "arm64" + + raise RuntimeError(f"Unsupported Windows machine architecture: {raw_machine!r}") + + def windows_pe_matches_arch(path: str, target_arch: str) -> bool: """Return whether a Windows Portable Executable (PE) targets the requested architecture. diff --git a/cuda_pathfinder/tests/test_find_nvidia_binaries.py b/cuda_pathfinder/tests/test_find_nvidia_binaries.py index 2784633ff38..f0aa0bab364 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_binaries.py +++ b/cuda_pathfinder/tests/test_find_nvidia_binaries.py @@ -128,6 +128,167 @@ def test_find_binary_windows_extension_and_search_dirs(monkeypatch, mocker): assert checked == [os.path.join(d, "nvcc.exe") for d in expected_dirs] +@pytest.mark.parametrize( + ("launcher_exists", "expected_rel", "checked_rels"), + ( + (True, os.path.join("bin", "compute-sanitizer.bat"), (os.path.join("bin", "compute-sanitizer.bat"),)), + ( + False, + os.path.join("compute-sanitizer", "compute-sanitizer.exe"), + ( + os.path.join("bin", "compute-sanitizer.bat"), + os.path.join("compute-sanitizer", "compute-sanitizer.exe"), + ), + ), + ), +) +@pytest.mark.usefixtures("clear_find_binary_cache") +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_compute_sanitizer_prefers_ctk_launcher_with_executable_fallback( + monkeypatch, mocker, launcher_exists, expected_rel, checked_rels +): + cuda_home = os.path.join(os.sep, "cuda") + launcher = os.path.join(cuda_home, "bin", "compute-sanitizer.bat") + executable = os.path.join(cuda_home, "compute-sanitizer", "compute-sanitizer.exe") + + mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) + mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) + monkeypatch.delenv("CONDA_PREFIX", raising=False) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) + canary_mock = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") + existing = [executable] + if launcher_exists: + existing.append(launcher) + checked = _patch_exec_probe(mocker, existing=existing) + + assert find_nvidia_binary_utility("compute-sanitizer") == os.path.abspath(os.path.join(cuda_home, expected_rel)) + assert checked == [os.path.join(cuda_home, rel) for rel in checked_rels] + canary_mock.assert_not_called() + + +@pytest.mark.parametrize( + ("machine_arch", "target_dir"), + ( + ("x64", "target-windows-x64"), + ("arm64", "target-windows-armv8"), + ), +) +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_windows_nsys_uses_machine_arch(mocker, machine_arch, target_dir): + install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") + expected = os.path.join(install_root, target_dir, "nsys.exe") + mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=install_root) + mocker.patch.object(binary_finder_module, "windows_machine_arch", return_value=machine_arch) + checked = _patch_exec_probe(mocker, existing=[expected]) + + assert binary_finder_module._find_windows_nsys() == os.path.abspath(expected) + assert checked == [expected] + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_windows_nsys_does_not_fallback_to_other_arch(mocker): + install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") + arm64 = os.path.join(install_root, "target-windows-armv8", "nsys.exe") + x64 = os.path.join(install_root, "target-windows-x64", "nsys.exe") + mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=install_root) + mocker.patch.object(binary_finder_module, "windows_machine_arch", return_value="arm64") + checked = _patch_exec_probe(mocker, existing=[x64]) + + assert binary_finder_module._find_windows_nsys() is None + assert checked == [arm64] + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_windows_ncu_prefers_launcher(mocker): + install_root = os.path.join(os.sep, "Program Files", "Nsight Compute") + launcher = os.path.join(install_root, "ncu.bat") + mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=install_root) + machine_arch_mock = mocker.patch.object(binary_finder_module, "windows_machine_arch") + checked = _patch_exec_probe(mocker, existing=[launcher]) + + assert binary_finder_module._find_windows_ncu() == os.path.abspath(launcher) + assert checked == [launcher] + machine_arch_mock.assert_not_called() + + +@pytest.mark.parametrize( + ("machine_arch", "target_dir"), + ( + ("x64", os.path.join("target", "windows-desktop-win7-x64")), + ("arm64", os.path.join("target", "windows-desktop-win10-t23x-a64")), + ), +) +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_windows_ncu_falls_back_to_machine_binary(mocker, machine_arch, target_dir): + install_root = os.path.join(os.sep, "Program Files", "Nsight Compute") + launcher = os.path.join(install_root, "ncu.bat") + expected = os.path.join(install_root, target_dir, "ncu.exe") + mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=install_root) + mocker.patch.object(binary_finder_module, "windows_machine_arch", return_value=machine_arch) + checked = _patch_exec_probe(mocker, existing=[expected]) + + assert binary_finder_module._find_windows_ncu() == os.path.abspath(expected) + assert checked == [launcher, expected] + + +@pytest.mark.parametrize( + ("utility_name", "finder_name"), + ( + ("nsys", "_find_windows_nsys"), + ("nsight-sys", "_find_windows_nsys"), + ("ncu", "_find_windows_ncu"), + ("nsight-compute", "_find_windows_ncu"), + ), +) +@pytest.mark.usefixtures("clear_find_binary_cache") +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_binary_uses_separately_installed_windows_nsight(monkeypatch, mocker, utility_name, finder_name): + expected = os.path.join(os.sep, "Program Files", utility_name) + mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) + mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) + monkeypatch.delenv("CONDA_PREFIX", raising=False) + installed_finder = mocker.patch.object(binary_finder_module, finder_name, return_value=expected) + get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") + canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") + + assert find_nvidia_binary_utility(utility_name) == expected + installed_finder.assert_called_once_with() + get_cuda_path.assert_not_called() + canary.assert_not_called() + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_windows_installed_nsight_root_reads_64_bit_registry(mocker): + install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") + product_key = mocker.MagicMock() + version_key = mocker.MagicMock() + product_context = mocker.MagicMock() + product_context.__enter__.return_value = product_key + version_context = mocker.MagicMock() + version_context.__enter__.return_value = version_key + winreg = mocker.MagicMock() + winreg.HKEY_LOCAL_MACHINE = object() + winreg.KEY_READ = 0x20019 + winreg.KEY_WOW64_64KEY = 0x0100 + winreg.OpenKey.side_effect = (product_context, version_context) + winreg.QueryValueEx.side_effect = (("2026.1.3", 1), (install_root, 1)) + mocker.patch.object(binary_finder_module.importlib, "import_module", return_value=winreg) + + assert binary_finder_module._windows_installed_nsight_root("Systems") == install_root + access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY + winreg.OpenKey.assert_has_calls( + ( + mocker.call( + winreg.HKEY_LOCAL_MACHINE, + rf"{binary_finder_module._NSIGHT_REGISTRY_ROOT}\Systems", + 0, + access, + ), + mocker.call(product_key, "2026.1.3", 0, access), + ) + ) + + @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_first_matching_dir_wins(monkeypatch, mocker): conda_prefix = os.path.join(os.sep, "conda") diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index 54136dc34e1..99ce416f1d6 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -147,6 +147,25 @@ def test_rejects_unknown_sysconfig_tag(self, mocker): assert exc_info.value.platform_tag == "custom-win" +class TestWindowsMachineArch: + @pytest.mark.parametrize( + ("reported_machine", "expected"), + (("AMD64", "x64"), ("x86_64", "x64"), ("ARM64", "arm64"), ("aarch64", "arm64")), + ) + @pytest.mark.agent_authored(model="gpt-5.6") + def test_normalizes_platform_machine(self, mocker, reported_machine, expected): + mocker.patch.object(windows_arch_mod.platform, "machine", return_value=reported_machine) + + assert windows_arch_mod.windows_machine_arch() == expected + + @pytest.mark.agent_authored(model="gpt-5.6") + def test_rejects_unknown_machine(self, mocker): + mocker.patch.object(windows_arch_mod.platform, "machine", return_value="mips64") + + with pytest.raises(RuntimeError, match=r"Unsupported Windows machine architecture: 'mips64'"): + windows_arch_mod.windows_machine_arch() + + @pytest.mark.parametrize( ("machine", "target_arch", "expected"), ( From 01d7dd2e5b9357f23980aba74e4a8e180f19cfec Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Sun, 9 Aug 2026 23:57:10 -0700 Subject: [PATCH 02/11] Clarify binary utility search order --- .../_binaries/find_nvidia_binary_utility.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 72fc049b64f..dd035e4ce86 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -195,21 +195,19 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: environment variable, which use platform-specific bin directory layouts (``Library/bin`` on Windows, ``bin`` on Linux). - 3. **Windows Nsight installations** + 3. **Library-specific standalone installation paths** - On Windows, locate Nsight Systems and Nsight Compute from their installer registry entries. Select architecture-specific binaries using the native machine architecture, independent of Python. - - 4. **CUDA Toolkit environment variables** - - - Use ``CUDA_HOME`` or ``CUDA_PATH`` (in that order), searching + - For utilities delivered with the CUDA Toolkit, use ``CUDA_HOME`` + or ``CUDA_PATH`` (in that order), searching ``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows, or just ``bin`` on Linux. - 5. **CTK-root canary fallback** + 4. **CTK-root canary fallback** - - Only when steps 1-4 miss: resolve the ``cudart`` library through the + - 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. From fa56eea99216d1b4addfda291d5d21d339fed740 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Sun, 9 Aug 2026 23:58:04 -0700 Subject: [PATCH 03/11] Expand standalone installation documentation --- .../_binaries/find_nvidia_binary_utility.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index dd035e4ce86..8e7fcffef39 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -195,15 +195,17 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: environment variable, which use platform-specific bin directory layouts (``Library/bin`` on Windows, ``bin`` on Linux). - 3. **Library-specific standalone installation paths** + 3. **Library-specific standalone installations** + - Search the installation paths for the CUDA Toolkit, Nsight Systems, + and Nsight Compute. + - For the CUDA Toolkit, use ``CUDA_HOME`` or ``CUDA_PATH`` (in that + order), searching + ``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows, + or just ``bin`` on Linux. - On Windows, locate Nsight Systems and Nsight Compute from their installer registry entries. Select architecture-specific binaries using the native machine architecture, independent of Python. - - For utilities delivered with the CUDA Toolkit, use ``CUDA_HOME`` - or ``CUDA_PATH`` (in that order), searching - ``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows, - or just ``bin`` on Linux. 4. **CTK-root canary fallback** From daf96fb8ecc7df63edd26ced04f91f1891d0a048 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Mon, 10 Aug 2026 00:00:38 -0700 Subject: [PATCH 04/11] Align standalone search step comments --- .../_binaries/find_nvidia_binary_utility.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 8e7fcffef39..425594cbce1 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -199,13 +199,15 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: - Search the installation paths for the CUDA Toolkit, Nsight Systems, and Nsight Compute. - - For the CUDA Toolkit, use ``CUDA_HOME`` or ``CUDA_PATH`` (in that - order), searching - ``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows, - or just ``bin`` on Linux. - - On Windows, locate Nsight Systems and Nsight Compute from their - installer registry entries. Select architecture-specific binaries - using the native machine architecture, independent of Python. + + 3.1. **Nsight installations**: On Windows, locate Nsight Systems and + Nsight Compute from their installer registry entries. Select + architecture-specific binaries using the native machine + architecture, independent of Python. + + 3.2. **CUDA Toolkit installation**: Use ``CUDA_HOME`` or ``CUDA_PATH`` + (in that order), searching ``bin/x64``, ``bin/x86_64``, and + ``bin`` subdirectories on Windows, or just ``bin`` on Linux. 4. **CTK-root canary fallback** @@ -260,13 +262,14 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: if found is not None: return found - # 3. Nsight tools are separate products and are not installed under CTK. + # 3. Search library-specific standalone installations. + # 3.1. Nsight tools are separate products and are not installed under CTK. if IS_WINDOWS and resolved_name == "nsys": return _find_windows_nsys() if IS_WINDOWS and resolved_name == "ncu": return _find_windows_ncu() - # 4. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH). + # 3.2. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH). if (cuda_home := get_cuda_path_or_home()) is not None: if IS_WINDOWS and resolved_name == "compute-sanitizer": found = _find_windows_compute_sanitizer(cuda_home) @@ -275,7 +278,7 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: if found is not None: return found - # 5. CTK-root canary fallback. + # 4. CTK-root canary fallback. ctk_root = _resolve_ctk_root_via_canary() if ctk_root is not None: if IS_WINDOWS and resolved_name == "compute-sanitizer": From 3a2fc317baf35e7c78027d15879ac59696b23a11 Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Mon, 10 Aug 2026 12:16:12 -0700 Subject: [PATCH 05/11] Preserve literal Nsight launcher lookup --- .../_binaries/find_nvidia_binary_utility.py | 18 +++--- .../_binaries/supported_nvidia_binaries.py | 6 -- .../tests/test_find_nvidia_binaries.py | 60 ++++++++++++++++++- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 425594cbce1..283d27f99d2 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -235,10 +235,6 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: if utility_name not in supported_nvidia_binaries.SUPPORTED_BINARIES: raise UnsupportedBinaryError(utility_name) - resolved_name = ( - supported_nvidia_binaries.WINDOWS_BINARY_ALIASES.get(utility_name, utility_name) if IS_WINDOWS else utility_name - ) - # 1. Search in site-packages (NVIDIA wheels) candidate_dirs = supported_nvidia_binaries.SITE_PACKAGES_BINDIRS.get(utility_name, ()) dirs = [] @@ -253,9 +249,9 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: else: dirs.append(os.path.join(conda_prefix, "bin")) - normalized_name = _normalize_utility_name(resolved_name) - if IS_WINDOWS and resolved_name in ("compute-sanitizer", "ncu"): - candidate_names = (f"{resolved_name}.bat", normalized_name) + normalized_name = _normalize_utility_name(utility_name) + if IS_WINDOWS and utility_name in ("compute-sanitizer", "ncu"): + candidate_names = (f"{utility_name}.bat", normalized_name) found = _resolve_names_in_trusted_dirs(candidate_names, dirs) else: found = _resolve_in_trusted_dirs(normalized_name, dirs) @@ -264,14 +260,14 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: # 3. Search library-specific standalone installations. # 3.1. Nsight tools are separate products and are not installed under CTK. - if IS_WINDOWS and resolved_name == "nsys": + if IS_WINDOWS and utility_name == "nsys": return _find_windows_nsys() - if IS_WINDOWS and resolved_name == "ncu": + if IS_WINDOWS and utility_name == "ncu": return _find_windows_ncu() # 3.2. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH). if (cuda_home := get_cuda_path_or_home()) is not None: - if IS_WINDOWS and resolved_name == "compute-sanitizer": + if IS_WINDOWS and utility_name == "compute-sanitizer": found = _find_windows_compute_sanitizer(cuda_home) else: found = _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(cuda_home)) @@ -281,7 +277,7 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: # 4. CTK-root canary fallback. ctk_root = _resolve_ctk_root_via_canary() if ctk_root is not None: - if IS_WINDOWS and resolved_name == "compute-sanitizer": + if IS_WINDOWS and utility_name == "compute-sanitizer": return _find_windows_compute_sanitizer(ctk_root) return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(ctk_root)) return None diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py b/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py index b5fb7428d6c..ac70378f112 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/supported_nvidia_binaries.py @@ -31,10 +31,4 @@ "nsight-compute": (_NSIGHT_COMPUTE_BIN,), } -# Names accepted by the public API that differ from the installed executable. -WINDOWS_BINARY_ALIASES = { - "nsight-sys": "nsys", - "nsight-compute": "ncu", -} - SUPPORTED_BINARIES_ALL = SUPPORTED_BINARIES = tuple(SITE_PACKAGES_BINDIRS.keys()) diff --git a/cuda_pathfinder/tests/test_find_nvidia_binaries.py b/cuda_pathfinder/tests/test_find_nvidia_binaries.py index f0aa0bab364..6a914e9b8fd 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_binaries.py +++ b/cuda_pathfinder/tests/test_find_nvidia_binaries.py @@ -235,9 +235,7 @@ def test_find_windows_ncu_falls_back_to_machine_binary(mocker, machine_arch, tar ("utility_name", "finder_name"), ( ("nsys", "_find_windows_nsys"), - ("nsight-sys", "_find_windows_nsys"), ("ncu", "_find_windows_ncu"), - ("nsight-compute", "_find_windows_ncu"), ), ) @pytest.mark.usefixtures("clear_find_binary_cache") @@ -257,6 +255,64 @@ def test_find_binary_uses_separately_installed_windows_nsight(monkeypatch, mocke canary.assert_not_called() +@pytest.mark.parametrize("utility_name", ("nsight-sys", "nsight-compute")) +@pytest.mark.usefixtures("clear_find_binary_cache") +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_windows_nsight_legacy_names_remain_literal_in_early_search(monkeypatch, mocker, utility_name): + site_key = os.path.join("nvidia", utility_name, "bin") + site_dir = os.path.join(os.sep, "site-packages", utility_name, "bin") + conda_prefix = os.path.join(os.sep, "conda") + conda_bin = os.path.join(conda_prefix, "Library", "bin") + expected = os.path.join(conda_bin, f"{utility_name}.exe") + + mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) + mocker.patch.object( + binary_finder_module.supported_nvidia_binaries, + "SITE_PACKAGES_BINDIRS", + {utility_name: (site_key,)}, + ) + find_sub_dirs = mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) + monkeypatch.setenv("CONDA_PREFIX", conda_prefix) + get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") + nsys_finder = mocker.patch.object(binary_finder_module, "_find_windows_nsys") + ncu_finder = mocker.patch.object(binary_finder_module, "_find_windows_ncu") + checked = _patch_exec_probe(mocker, existing=[expected]) + + assert find_nvidia_binary_utility(utility_name) == os.path.abspath(expected) + assert checked == [os.path.join(site_dir, f"{utility_name}.exe"), expected] + find_sub_dirs.assert_called_once_with(site_key.split(os.sep)) + get_cuda_path.assert_not_called() + nsys_finder.assert_not_called() + ncu_finder.assert_not_called() + + +@pytest.mark.parametrize("utility_name", ("nsight-sys", "nsight-compute")) +@pytest.mark.usefixtures("clear_find_binary_cache") +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_windows_nsight_legacy_names_remain_literal_in_ctk(monkeypatch, mocker, utility_name): + cuda_home = os.path.join(os.sep, "cuda") + expected = os.path.join(cuda_home, "bin", f"{utility_name}.exe") + + mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) + mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) + monkeypatch.delenv("CONDA_PREFIX", raising=False) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) + nsys_finder = mocker.patch.object(binary_finder_module, "_find_windows_nsys") + ncu_finder = mocker.patch.object(binary_finder_module, "_find_windows_ncu") + canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") + checked = _patch_exec_probe(mocker, existing=[expected]) + + assert find_nvidia_binary_utility(utility_name) == os.path.abspath(expected) + assert checked == [ + os.path.join(cuda_home, "bin", "x64", f"{utility_name}.exe"), + os.path.join(cuda_home, "bin", "x86_64", f"{utility_name}.exe"), + expected, + ] + nsys_finder.assert_not_called() + ncu_finder.assert_not_called() + canary.assert_not_called() + + @pytest.mark.agent_authored(model="gpt-5.6") def test_windows_installed_nsight_root_reads_64_bit_registry(mocker): install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") From e220d5bfcfad8e3e7e9efa9b5515b4118474533a Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Mon, 10 Aug 2026 12:51:57 -0700 Subject: [PATCH 06/11] Cover Windows binary discovery fallbacks --- .../_binaries/find_nvidia_binary_utility.py | 12 +- .../tests/test_find_nvidia_binaries.py | 119 ++++++++++++++++-- 2 files changed, 118 insertions(+), 13 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 283d27f99d2..baae9929a4e 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -203,7 +203,9 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: 3.1. **Nsight installations**: On Windows, locate Nsight Systems and Nsight Compute from their installer registry entries. Select architecture-specific binaries using the native machine - architecture, independent of Python. + architecture, independent of Python. Lookup of the standalone + ``nsys`` and ``ncu`` CLIs is terminal; a miss does not fall + through to CUDA Toolkit locations. 3.2. **CUDA Toolkit installation**: Use ``CUDA_HOME`` or ``CUDA_PATH`` (in that order), searching ``bin/x64``, ``bin/x86_64``, and @@ -211,9 +213,9 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: 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. + - For utilities that reach this step after the earlier searches 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 @@ -259,7 +261,7 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: return found # 3. Search library-specific standalone installations. - # 3.1. Nsight tools are separate products and are not installed under CTK. + # 3.1. Standalone Nsight CLI lookup is terminal; CTK does not contain nsys/ncu. if IS_WINDOWS and utility_name == "nsys": return _find_windows_nsys() if IS_WINDOWS and utility_name == "ncu": diff --git a/cuda_pathfinder/tests/test_find_nvidia_binaries.py b/cuda_pathfinder/tests/test_find_nvidia_binaries.py index 6a914e9b8fd..f2c424088ad 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_binaries.py +++ b/cuda_pathfinder/tests/test_find_nvidia_binaries.py @@ -166,6 +166,24 @@ def test_find_compute_sanitizer_prefers_ctk_launcher_with_executable_fallback( canary_mock.assert_not_called() +@pytest.mark.usefixtures("clear_find_binary_cache") +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_compute_sanitizer_uses_canary_ctk_root(monkeypatch, mocker): + ctk_root = os.path.join(os.sep, "cuda") + launcher = os.path.join(ctk_root, "bin", "compute-sanitizer.bat") + + mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) + mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) + monkeypatch.delenv("CONDA_PREFIX", raising=False) + mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=None) + canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary", return_value=ctk_root) + checked = _patch_exec_probe(mocker, existing=[launcher]) + + assert find_nvidia_binary_utility("compute-sanitizer") == os.path.abspath(launcher) + assert checked == [launcher] + canary.assert_called_once_with() + + @pytest.mark.parametrize( ("machine_arch", "target_dir"), ( @@ -232,25 +250,110 @@ def test_find_windows_ncu_falls_back_to_machine_binary(mocker, machine_arch, tar @pytest.mark.parametrize( - ("utility_name", "finder_name"), + ("utility_name", "candidate_names"), ( - ("nsys", "_find_windows_nsys"), - ("ncu", "_find_windows_ncu"), + ("nsys", ("nsys.exe",)), + ("ncu", ("ncu.bat", "ncu.exe")), ), ) @pytest.mark.usefixtures("clear_find_binary_cache") @pytest.mark.agent_authored(model="gpt-5.6") -def test_find_binary_uses_separately_installed_windows_nsight(monkeypatch, mocker, utility_name, finder_name): - expected = os.path.join(os.sep, "Program Files", utility_name) +def test_find_binary_windows_nsight_conda_precedes_registry(monkeypatch, mocker, utility_name, candidate_names): + site_dir = os.path.join(os.sep, "site-packages", utility_name, "bin") + conda_prefix = os.path.join(os.sep, "conda") + conda_bin = os.path.join(conda_prefix, "Library", "bin") + expected = os.path.join(conda_bin, candidate_names[0]) + + mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) + monkeypatch.setenv("CONDA_PREFIX", conda_prefix) + registry_root = mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root") + machine_arch = mocker.patch.object(binary_finder_module, "windows_machine_arch") + get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") + canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") + checked = _patch_exec_probe(mocker, existing=[expected]) + + assert find_nvidia_binary_utility(utility_name) == os.path.abspath(expected) + assert checked == [ + *(os.path.join(site_dir, name) for name in candidate_names), + os.path.join(conda_bin, candidate_names[0]), + ] + registry_root.assert_not_called() + machine_arch.assert_not_called() + get_cuda_path.assert_not_called() + canary.assert_not_called() + + +@pytest.mark.parametrize( + ("utility_name", "product", "machine_arch", "target_rel", "candidate_names"), + ( + ("nsys", "Systems", "x64", os.path.join("target-windows-x64", "nsys.exe"), ("nsys.exe",)), + ("nsys", "Systems", "arm64", os.path.join("target-windows-armv8", "nsys.exe"), ("nsys.exe",)), + ( + "ncu", + "Compute", + "x64", + os.path.join("target", "windows-desktop-win7-x64", "ncu.exe"), + ("ncu.bat", "ncu.exe"), + ), + ( + "ncu", + "Compute", + "arm64", + os.path.join("target", "windows-desktop-win10-t23x-a64", "ncu.exe"), + ("ncu.bat", "ncu.exe"), + ), + ), +) +@pytest.mark.usefixtures("clear_find_binary_cache") +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_binary_windows_nsight_composes_registry_and_native_target( + monkeypatch, mocker, utility_name, product, machine_arch, target_rel, candidate_names +): + site_dir = os.path.join(os.sep, "site-packages", utility_name, "bin") + conda_prefix = os.path.join(os.sep, "conda") + conda_bin = os.path.join(conda_prefix, "Library", "bin") + install_root = os.path.join(os.sep, "Program Files", utility_name) + expected = os.path.join(install_root, target_rel) + + mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) + mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) + monkeypatch.setenv("CONDA_PREFIX", conda_prefix) + registry_root = mocker.patch.object( + binary_finder_module, "_windows_installed_nsight_root", return_value=install_root + ) + machine_arch_mock = mocker.patch.object(binary_finder_module, "windows_machine_arch", return_value=machine_arch) + get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") + canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") + checked = _patch_exec_probe(mocker, existing=[expected]) + + assert find_nvidia_binary_utility(utility_name) == os.path.abspath(expected) + assert checked == [ + *(os.path.join(directory, name) for directory in (site_dir, conda_bin) for name in candidate_names), + *((os.path.join(install_root, "ncu.bat"),) if utility_name == "ncu" else ()), + expected, + ] + registry_root.assert_called_once_with(product) + machine_arch_mock.assert_called_once_with() + get_cuda_path.assert_not_called() + canary.assert_not_called() + + +@pytest.mark.parametrize(("utility_name", "product"), (("nsys", "Systems"), ("ncu", "Compute"))) +@pytest.mark.usefixtures("clear_find_binary_cache") +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_binary_windows_nsight_registry_miss_is_terminal(monkeypatch, mocker, utility_name, product): mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) monkeypatch.delenv("CONDA_PREFIX", raising=False) - installed_finder = mocker.patch.object(binary_finder_module, finder_name, return_value=expected) + registry_root = mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=None) + machine_arch = mocker.patch.object(binary_finder_module, "windows_machine_arch") get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") - assert find_nvidia_binary_utility(utility_name) == expected - installed_finder.assert_called_once_with() + assert find_nvidia_binary_utility(utility_name) is None + registry_root.assert_called_once_with(product) + machine_arch.assert_not_called() get_cuda_path.assert_not_called() canary.assert_not_called() From d063eb3cb22ecc659d3e891676d8e4c54426ca6a Mon Sep 17 00:00:00 2001 From: Michael Wang Date: Mon, 10 Aug 2026 14:06:02 -0700 Subject: [PATCH 07/11] Document Windows architecture selection --- .../pathfinder/_binaries/find_nvidia_binary_utility.py | 10 ++++++++++ .../_dynamic_libs/load_nvidia_dynamic_lib.py | 9 +++++++++ .../cuda/pathfinder/_static_libs/find_static_lib.py | 8 ++++++++ 3 files changed, 27 insertions(+) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index baae9929a4e..fa28347a399 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -183,6 +183,16 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: UnsupportedBinaryError: If ``utility_name`` is not in the supported set (see ``SUPPORTED_BINARY_UTILITIES``). + Windows on ARM (WoA) Note: + Binary utilities execute in separate processes and do not need to match + the Python process architecture. When choosing among architecture-specific + Windows layouts, this API deliberately targets the native machine + architecture rather than the Python interpreter architecture. For + example, standalone ``nsys`` and ``ncu`` discovery under x64 Python on an + Arm64 machine selects the Arm64 target. This differs from + ``load_nvidia_dynamic_lib`` and ``find_static_lib``, which target the + Python interpreter architecture. + Search order: 1. **NVIDIA Python wheels** 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 53446107da3..61bf31720d3 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 @@ -231,6 +231,15 @@ def load_nvidia_dynamic_lib(libname: str) -> LoadedDL: DynamicLibNotFoundError: If the library cannot be found or loaded. RuntimeError: If Python is not 64-bit. + Windows on ARM (WoA) Note: + On Windows, this API aims to load a dynamic library whose architecture + matches the Python interpreter architecture. For example, x64 Python + running on an Arm64 machine targets an x64 DLL, while native Arm64 Python + targets an Arm64 DLL. A library loaded into the Python process must be + compatible with that process. This differs from + ``find_nvidia_binary_utility``, which targets the native machine + architecture when selecting architecture-specific executables. + Search order: 0. **Already loaded in the current process** diff --git a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py index ea5a740aec4..8e0d81cab01 100644 --- a/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_static_libs/find_static_lib.py @@ -175,5 +175,13 @@ def find_static_lib(name: str) -> str: Raises: ValueError: If ``name`` is not a supported static library. StaticLibNotFoundError: If the static library cannot be found. + + Windows on ARM (WoA) Note: + On Windows, this API aims to return the path to a static library whose + architecture matches the Python interpreter architecture. For example, + x64 Python running on an Arm64 machine targets the x64 library, while + native Arm64 Python targets the Arm64 library. This differs from + ``find_nvidia_binary_utility``, which targets the native machine + architecture when selecting architecture-specific executables. """ return locate_static_lib(name).abs_path From 86dcf9c618e5e70c5433dc0d372f316bb9ac6ee6 Mon Sep 17 00:00:00 2001 From: isvoid Date: Tue, 11 Aug 2026 11:58:15 -0700 Subject: [PATCH 08/11] Harden Windows Arm64 utility discovery --- .../_binaries/find_nvidia_binary_utility.py | 32 ++++-- .../cuda/pathfinder/_utils/windows_arch.py | 62 +++++++++- cuda_pathfinder/docs/source/install.rst | 2 +- .../tests/test_find_nvidia_binaries.py | 108 +++++++++++++++++- cuda_pathfinder/tests/test_search_steps.py | 72 +++++++++++- 5 files changed, 255 insertions(+), 21 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index fa28347a399..2c784675cc5 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -91,17 +91,30 @@ def _windows_installed_nsight_root(product: str) -> str | None: access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY product_key_path = rf"{_NSIGHT_REGISTRY_ROOT}\{product}" try: - with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, product_key_path, 0, access) as product_key: - current_version, _ = winreg.QueryValueEx(product_key, "CurrentVersion") - if not isinstance(current_version, str) or not current_version: - raise RuntimeError(f"Invalid CurrentVersion value in {product_key_path!r}") - with winreg.OpenKey(product_key, current_version, 0, access) as version_key: - install_root, _ = winreg.QueryValueEx(version_key, None) + product_context = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, product_key_path, 0, access) except FileNotFoundError: return None - if not isinstance(install_root, str) or not install_root: - raise RuntimeError(f"Invalid installation directory for {product_key_path!r} version {current_version!r}") + try: + with product_context as product_key: + current_version, _ = winreg.QueryValueEx(product_key, "CurrentVersion") + if not isinstance(current_version, str) or not current_version.strip(): + raise RuntimeError( + f"Invalid CurrentVersion value {current_version!r} in " + f"Nsight {product!r} registry registration at {product_key_path!r}" + ) + with winreg.OpenKey(product_key, current_version, 0, access) as version_key: + install_root, _ = winreg.QueryValueEx(version_key, None) + except FileNotFoundError as exc: + raise RuntimeError( + f"Incomplete Nsight {product!r} registry registration at {product_key_path!r}" + ) from exc + + if not isinstance(install_root, str) or not install_root.strip(): + raise RuntimeError( + f"Invalid installation directory {install_root!r} in Nsight {product!r} " + f"registry registration at {product_key_path!r} version {current_version!r}" + ) return install_root @@ -182,6 +195,9 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: Raises: UnsupportedBinaryError: If ``utility_name`` is not in the supported set (see ``SUPPORTED_BINARY_UTILITIES``). + RuntimeError: If a native Windows architecture needed for an + architecture-specific utility layout cannot be determined, or an + installed Nsight product has incomplete or invalid registry data. Windows on ARM (WoA) Note: Binary utilities execute in separate processes and do not need to match diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py b/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py index d7b6264d16b..96eb53a4e69 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py @@ -11,6 +11,8 @@ "arm64": 0xAA64, } +_WINDOWS_ARCH_BY_PE_MACHINE = {machine: arch for arch, machine in WINDOWS_PE_MACHINE_BY_ARCH.items()} + class UnsupportedArchError(RuntimeError): """Raised when Python reports an unsupported Windows architecture.""" @@ -36,8 +38,8 @@ def windows_python_arch() -> str: raise UnsupportedArchError(raw_platform_tag) -def windows_machine_arch() -> str: - """Return the native Windows machine architecture.""" +def _windows_machine_arch_from_platform() -> str: + """Return the Windows architecture reported by Python's platform module.""" raw_machine = platform.machine() machine = raw_machine.lower().replace("_", "-") @@ -50,6 +52,62 @@ def windows_machine_arch() -> str: raise RuntimeError(f"Unsupported Windows machine architecture: {raw_machine!r}") +def _windows_native_machine() -> int | None: + """Return the native Windows PE machine type, or None on older Windows.""" + import ctypes + from ctypes import wintypes + + try: + # These ctypes attributes are absent from the type stubs on non-Windows hosts. + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined] + except OSError as exc: + raise RuntimeError("Failed to load kernel32 while detecting the native Windows architecture") from exc + + get_current_process = kernel32.GetCurrentProcess + try: + is_wow64_process2 = kernel32.IsWow64Process2 + except AttributeError: + return None + + get_current_process.argtypes = () + get_current_process.restype = wintypes.HANDLE + is_wow64_process2.argtypes = ( + wintypes.HANDLE, + ctypes.POINTER(wintypes.USHORT), + ctypes.POINTER(wintypes.USHORT), + ) + is_wow64_process2.restype = wintypes.BOOL + + process_machine = wintypes.USHORT() + native_machine = wintypes.USHORT() + if not is_wow64_process2( + get_current_process(), + ctypes.byref(process_machine), + ctypes.byref(native_machine), + ): + error_code = ctypes.get_last_error() # type: ignore[attr-defined] + error = ctypes.WinError(error_code) # type: ignore[attr-defined] + raise RuntimeError( + f"IsWow64Process2 failed while detecting the native Windows architecture " + f"(Windows error {error_code}): {error}" + ) from error + return native_machine.value + + +def windows_machine_arch() -> str: + """Return the native Windows machine architecture, ignoring process emulation.""" + native_machine = _windows_native_machine() + if native_machine is None: + # IsWow64Process2 predates x64-on-Arm emulation, so this fallback is only + # needed on older Windows versions where platform.machine() is sufficient. + return _windows_machine_arch_from_platform() + + try: + return _WINDOWS_ARCH_BY_PE_MACHINE[native_machine] + except KeyError: + raise RuntimeError(f"Unsupported native Windows PE machine type: 0x{native_machine:04x}") from None + + def windows_pe_matches_arch(path: str, target_arch: str) -> bool: """Return whether a Windows Portable Executable (PE) targets the requested architecture. diff --git a/cuda_pathfinder/docs/source/install.rst b/cuda_pathfinder/docs/source/install.rst index 53f11ebbf18..078abf47ee3 100644 --- a/cuda_pathfinder/docs/source/install.rst +++ b/cuda_pathfinder/docs/source/install.rst @@ -9,7 +9,7 @@ Runtime Requirements ``cuda.pathfinder`` is a pure-Python package with no runtime dependencies: -* Linux (x86-64, arm64) and Windows (x86-64) +* Linux (x86-64, arm64) and Windows (x86-64, arm64) * Python 3.10 - 3.14 Installing from PyPI diff --git a/cuda_pathfinder/tests/test_find_nvidia_binaries.py b/cuda_pathfinder/tests/test_find_nvidia_binaries.py index f2c424088ad..668be893863 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_binaries.py +++ b/cuda_pathfinder/tests/test_find_nvidia_binaries.py @@ -58,6 +58,15 @@ def fake_is_executable_candidate(path): return checked +def _patch_winreg(mocker): + winreg = mocker.MagicMock() + winreg.HKEY_LOCAL_MACHINE = object() + winreg.KEY_READ = 0x20019 + winreg.KEY_WOW64_64KEY = 0x0100 + mocker.patch.object(binary_finder_module.importlib, "import_module", return_value=winreg) + return winreg + + @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_search_path_includes_site_packages_conda_cuda(monkeypatch, mocker): conda_prefix = os.path.join(os.sep, "conda") @@ -425,13 +434,9 @@ def test_windows_installed_nsight_root_reads_64_bit_registry(mocker): product_context.__enter__.return_value = product_key version_context = mocker.MagicMock() version_context.__enter__.return_value = version_key - winreg = mocker.MagicMock() - winreg.HKEY_LOCAL_MACHINE = object() - winreg.KEY_READ = 0x20019 - winreg.KEY_WOW64_64KEY = 0x0100 + winreg = _patch_winreg(mocker) winreg.OpenKey.side_effect = (product_context, version_context) winreg.QueryValueEx.side_effect = (("2026.1.3", 1), (install_root, 1)) - mocker.patch.object(binary_finder_module.importlib, "import_module", return_value=winreg) assert binary_finder_module._windows_installed_nsight_root("Systems") == install_root access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY @@ -448,6 +453,99 @@ def test_windows_installed_nsight_root_reads_64_bit_registry(mocker): ) +@pytest.mark.agent_authored(model="gpt-5.6") +def test_windows_installed_nsight_root_returns_none_when_product_key_is_absent(mocker): + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = FileNotFoundError("Nsight Systems is not installed") + + assert binary_finder_module._windows_installed_nsight_root("Systems") is None + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_windows_installed_nsight_root_rejects_missing_current_version(mocker): + product_context = mocker.MagicMock() + product_context.__enter__.return_value = mocker.MagicMock() + winreg = _patch_winreg(mocker) + winreg.OpenKey.return_value = product_context + winreg.QueryValueEx.side_effect = FileNotFoundError("CurrentVersion is missing") + + with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info: + binary_finder_module._windows_installed_nsight_root("Systems") + + assert isinstance(exc_info.value.__cause__, FileNotFoundError) + + +@pytest.mark.parametrize("current_version", (None, "", " ", 2026)) +@pytest.mark.agent_authored(model="gpt-5.6") +def test_windows_installed_nsight_root_rejects_invalid_current_version(mocker, current_version): + product_context = mocker.MagicMock() + product_context.__enter__.return_value = mocker.MagicMock() + winreg = _patch_winreg(mocker) + winreg.OpenKey.return_value = product_context + winreg.QueryValueEx.return_value = (current_version, 1) + + with pytest.raises(RuntimeError, match=r"Invalid CurrentVersion value .*Nsight 'Systems' registry registration"): + binary_finder_module._windows_installed_nsight_root("Systems") + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_windows_installed_nsight_root_rejects_missing_version_key(mocker): + product_key = mocker.MagicMock() + product_context = mocker.MagicMock() + product_context.__enter__.return_value = product_key + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = (product_context, FileNotFoundError("Version key is missing")) + winreg.QueryValueEx.return_value = ("2026.1.3", 1) + + with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info: + binary_finder_module._windows_installed_nsight_root("Systems") + + assert isinstance(exc_info.value.__cause__, FileNotFoundError) + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_windows_installed_nsight_root_rejects_missing_installation_directory(mocker): + product_context = mocker.MagicMock() + product_context.__enter__.return_value = mocker.MagicMock() + version_context = mocker.MagicMock() + version_context.__enter__.return_value = mocker.MagicMock() + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = (product_context, version_context) + winreg.QueryValueEx.side_effect = (("2026.1.3", 1), FileNotFoundError("Installation directory is missing")) + + with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info: + binary_finder_module._windows_installed_nsight_root("Systems") + + assert isinstance(exc_info.value.__cause__, FileNotFoundError) + + +@pytest.mark.parametrize("install_root", (None, "", " ", 2026)) +@pytest.mark.agent_authored(model="gpt-5.6") +def test_windows_installed_nsight_root_rejects_invalid_installation_directory(mocker, install_root): + product_context = mocker.MagicMock() + product_context.__enter__.return_value = mocker.MagicMock() + version_context = mocker.MagicMock() + version_context.__enter__.return_value = mocker.MagicMock() + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = (product_context, version_context) + winreg.QueryValueEx.side_effect = (("2026.1.3", 1), (install_root, 1)) + + with pytest.raises( + RuntimeError, + match=r"Invalid installation directory .*Nsight 'Systems' registry registration.*version '2026.1.3'", + ): + binary_finder_module._windows_installed_nsight_root("Systems") + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_windows_installed_nsight_root_propagates_access_errors(mocker): + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = PermissionError("Registry access denied") + + with pytest.raises(PermissionError, match="Registry access denied"): + binary_finder_module._windows_installed_nsight_root("Systems") + + @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_first_matching_dir_wins(monkeypatch, mocker): conda_prefix = os.path.join(os.sep, "conda") diff --git a/cuda_pathfinder/tests/test_search_steps.py b/cuda_pathfinder/tests/test_search_steps.py index 99ce416f1d6..fc78e22c708 100644 --- a/cuda_pathfinder/tests/test_search_steps.py +++ b/cuda_pathfinder/tests/test_search_steps.py @@ -5,7 +5,9 @@ from __future__ import annotations +import ctypes import os +from ctypes import wintypes import pytest @@ -148,22 +150,82 @@ def test_rejects_unknown_sysconfig_tag(self, mocker): class TestWindowsMachineArch: + @pytest.mark.parametrize( + ("native_machine", "expected"), + ((0x8664, "x64"), (0xAA64, "arm64")), + ) + @pytest.mark.agent_authored(model="gpt-5.6") + def test_uses_native_pe_machine(self, mocker, native_machine, expected): + mocker.patch.object(windows_arch_mod, "_windows_native_machine", return_value=native_machine) + platform_machine = mocker.patch.object(windows_arch_mod.platform, "machine", return_value="AMD64") + + assert windows_arch_mod.windows_machine_arch() == expected + platform_machine.assert_not_called() + + @pytest.mark.agent_authored(model="gpt-5.6") + def test_rejects_unknown_native_pe_machine(self, mocker): + mocker.patch.object(windows_arch_mod, "_windows_native_machine", return_value=0x014C) + + with pytest.raises(RuntimeError, match=r"Unsupported native Windows PE machine type: 0x014c"): + windows_arch_mod.windows_machine_arch() + @pytest.mark.parametrize( ("reported_machine", "expected"), (("AMD64", "x64"), ("x86_64", "x64"), ("ARM64", "arm64"), ("aarch64", "arm64")), ) @pytest.mark.agent_authored(model="gpt-5.6") - def test_normalizes_platform_machine(self, mocker, reported_machine, expected): + def test_old_windows_fallback_normalizes_platform_machine(self, mocker, reported_machine, expected): + mocker.patch.object(windows_arch_mod, "_windows_native_machine", return_value=None) mocker.patch.object(windows_arch_mod.platform, "machine", return_value=reported_machine) assert windows_arch_mod.windows_machine_arch() == expected @pytest.mark.agent_authored(model="gpt-5.6") - def test_rejects_unknown_machine(self, mocker): - mocker.patch.object(windows_arch_mod.platform, "machine", return_value="mips64") + def test_native_machine_returns_none_when_is_wow64_process2_is_unavailable(self, mocker): + kernel32 = mocker.Mock(spec=["GetCurrentProcess"]) + mocker.patch.object(ctypes, "WinDLL", create=True, return_value=kernel32) - with pytest.raises(RuntimeError, match=r"Unsupported Windows machine architecture: 'mips64'"): - windows_arch_mod.windows_machine_arch() + assert windows_arch_mod._windows_native_machine() is None + + @pytest.mark.agent_authored(model="gpt-5.6") + def test_native_machine_configures_api_and_returns_native_machine(self, mocker): + kernel32 = mocker.Mock() + kernel32.GetCurrentProcess.return_value = wintypes.HANDLE(1) + + def report_native_machine(_process, _process_machine, native_machine): + native_machine._obj.value = 0xAA64 + return True + + kernel32.IsWow64Process2.side_effect = report_native_machine + mocker.patch.object(ctypes, "WinDLL", create=True, return_value=kernel32) + + assert windows_arch_mod._windows_native_machine() == 0xAA64 + assert kernel32.GetCurrentProcess.argtypes == () + assert kernel32.GetCurrentProcess.restype is wintypes.HANDLE + assert kernel32.IsWow64Process2.argtypes == ( + wintypes.HANDLE, + ctypes.POINTER(wintypes.USHORT), + ctypes.POINTER(wintypes.USHORT), + ) + assert kernel32.IsWow64Process2.restype is wintypes.BOOL + + @pytest.mark.agent_authored(model="gpt-5.6") + def test_native_machine_raises_contextual_error_when_api_call_fails(self, mocker): + kernel32 = mocker.Mock() + kernel32.GetCurrentProcess.return_value = wintypes.HANDLE(1) + kernel32.IsWow64Process2.return_value = False + mocker.patch.object(ctypes, "WinDLL", create=True, return_value=kernel32) + mocker.patch.object(ctypes, "get_last_error", create=True, return_value=87) + windows_error = OSError(87, "The parameter is incorrect") + mocker.patch.object(ctypes, "WinError", create=True, return_value=windows_error) + + with pytest.raises( + RuntimeError, + match=r"IsWow64Process2 failed while detecting the native Windows architecture \(Windows error 87\)", + ) as exc_info: + windows_arch_mod._windows_native_machine() + + assert exc_info.value.__cause__ is windows_error @pytest.mark.parametrize( From f985d6d257dbdd72a86d6d5a9550ffe664b1dc9c Mon Sep 17 00:00:00 2001 From: isvoid <13521008+isVoid@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:35:56 -0700 Subject: [PATCH 09/11] Fix Windows pre-commit checks --- .../cuda/pathfinder/_binaries/find_nvidia_binary_utility.py | 4 +--- cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 2c784675cc5..6ac8da8ccc0 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -106,9 +106,7 @@ def _windows_installed_nsight_root(product: str) -> str | None: with winreg.OpenKey(product_key, current_version, 0, access) as version_key: install_root, _ = winreg.QueryValueEx(version_key, None) except FileNotFoundError as exc: - raise RuntimeError( - f"Incomplete Nsight {product!r} registry registration at {product_key_path!r}" - ) from exc + raise RuntimeError(f"Incomplete Nsight {product!r} registry registration at {product_key_path!r}") from exc if not isinstance(install_root, str) or not install_root.strip(): raise RuntimeError( diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py b/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py index 96eb53a4e69..fc802db370f 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/windows_arch.py @@ -59,7 +59,7 @@ def _windows_native_machine() -> int | None: try: # These ctypes attributes are absent from the type stubs on non-Windows hosts. - kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined] + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore[attr-defined, unused-ignore] except OSError as exc: raise RuntimeError("Failed to load kernel32 while detecting the native Windows architecture") from exc @@ -85,8 +85,8 @@ def _windows_native_machine() -> int | None: ctypes.byref(process_machine), ctypes.byref(native_machine), ): - error_code = ctypes.get_last_error() # type: ignore[attr-defined] - error = ctypes.WinError(error_code) # type: ignore[attr-defined] + error_code = ctypes.get_last_error() # type: ignore[attr-defined, unused-ignore] + error = ctypes.WinError(error_code) # type: ignore[attr-defined, unused-ignore] raise RuntimeError( f"IsWow64Process2 failed while detecting the native Windows architecture " f"(Windows error {error_code}): {error}" From b0c2a775d6ae857a7e81dc38de8a4aaa75d61080 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 12 Aug 2026 00:42:17 -0700 Subject: [PATCH 10/11] Fix CUDA path precedence documentation --- .../pathfinder/_binaries/find_nvidia_binary_utility.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 6ac8da8ccc0..28082507238 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -231,7 +231,7 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: ``nsys`` and ``ncu`` CLIs is terminal; a miss does not fall through to CUDA Toolkit locations. - 3.2. **CUDA Toolkit installation**: Use ``CUDA_HOME`` or ``CUDA_PATH`` + 3.2. **CUDA Toolkit installation**: Use ``CUDA_PATH`` or ``CUDA_HOME`` (in that order), searching ``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows, or just ``bin`` on Linux. @@ -291,12 +291,12 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: if IS_WINDOWS and utility_name == "ncu": return _find_windows_ncu() - # 3.2. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH). - if (cuda_home := get_cuda_path_or_home()) is not None: + # 3.2. Search in CUDA Toolkit (CUDA_PATH/CUDA_HOME). + if (cuda_path := get_cuda_path_or_home()) is not None: if IS_WINDOWS and utility_name == "compute-sanitizer": - found = _find_windows_compute_sanitizer(cuda_home) + found = _find_windows_compute_sanitizer(cuda_path) else: - found = _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(cuda_home)) + found = _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(cuda_path)) if found is not None: return found From f8763641034765af04045417f77c44e29b8dc58c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 12 Aug 2026 01:14:19 -0700 Subject: [PATCH 11/11] Document Windows binary utility discovery --- cuda_pathfinder/docs/source/release/1.6.1-notes.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cuda_pathfinder/docs/source/release/1.6.1-notes.rst b/cuda_pathfinder/docs/source/release/1.6.1-notes.rst index 919963802ff..4b6e7a0a55e 100644 --- a/cuda_pathfinder/docs/source/release/1.6.1-notes.rst +++ b/cuda_pathfinder/docs/source/release/1.6.1-notes.rst @@ -29,6 +29,12 @@ Highlights ``lib/x64`` or ``lib/arm64`` CUDA Toolkit and wheel directories. CUDA 12 component-wheel and legacy Conda fallbacks remain x64-only. +* Fix Windows binary-utility discovery for CUDA 13.4 Arm64 layouts. Prefer the + Compute Sanitizer launcher, locate standalone Nsight Systems and Nsight + Compute through their installer registry entries, and select + architecture-specific executable targets using the native Windows machine + architecture. + Internal maintenance --------------------