Skip to content
Draft
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 @@ -158,8 +158,9 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
Nsight Compute from their installer registry entries. Select
architecture-specific binaries using the native machine
architecture, independent of Python. Lookup of the standalone
``nsys`` and ``ncu`` CLIs is terminal; a miss does not fall
through to CUDA Toolkit locations.
``nsys``/``ncu`` CLIs and the ``nsys-ui``/``ncu-ui`` GUI
launchers is terminal; a miss does not fall through to CUDA
Toolkit locations.

3.2. **CUDA Toolkit installation**: Use ``CUDA_PATH`` or ``CUDA_HOME``
(in that order), searching ``bin/x64``, ``bin/x86_64``, and
Expand Down Expand Up @@ -215,11 +216,16 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
return found

# 3. Search library-specific standalone installations.
# 3.1. Standalone Nsight CLI lookup is terminal; CTK does not contain nsys/ncu.
# 3.1. Standalone Nsight lookup is terminal; CTK does not contain nsys/ncu
# (nor the nsys-ui/ncu-ui GUI launchers).
if IS_WINDOWS and utility_name == "nsys":
return _resolve_candidate_paths(windows_nsight.nsys_candidate_paths())
if IS_WINDOWS and utility_name == "ncu":
return _resolve_candidate_paths(windows_nsight.ncu_candidate_paths())
if IS_WINDOWS and utility_name == "nsys-ui":
return _resolve_candidate_paths(windows_nsight.nsys_ui_candidate_paths())
if IS_WINDOWS and utility_name == "ncu-ui":
return _resolve_candidate_paths(windows_nsight.ncu_ui_candidate_paths())

# 3.2. Search in CUDA Toolkit (CUDA_PATH/CUDA_HOME).
if (cuda_path := get_cuda_path_or_home()) is not None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@
"compute-sanitizer": (_CUDA13_BIN, _CUDA_NVCC_BIN),
# Profiling tools
"nvprof": (_CUDA_NVCC_BIN,),
# Nsight Systems/Compute ship both a CLI (nsys/ncu) and a GUI (nsys-ui/ncu-ui).
# The GUI executable names differ between the legacy CUDA-toolkit-bundled
# products (nsight-sys/nsight-compute) and the current standalone Nsight
# releases (nsys-ui/ncu-ui), so all four names are kept searchable.
"nsys": (_NSIGHT_SYSTEMS_BIN,),
"nsys-ui": (_NSIGHT_SYSTEMS_BIN,),
"nsight-sys": (_NSIGHT_SYSTEMS_BIN,),
"ncu": (_NSIGHT_COMPUTE_BIN,),
"ncu-ui": (_NSIGHT_COMPUTE_BIN,),
"nsight-compute": (_NSIGHT_COMPUTE_BIN,),
}

Expand Down
31 changes: 31 additions & 0 deletions cuda_pathfinder/cuda/pathfinder/_binaries/windows_nsight.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@
"arm64": os.path.join("target", "windows-desktop-win10-t23x-a64"),
}

# The standalone Nsight GUI launchers live under the host directories, mirrored
# from the CLI target directories above. The GUI executable names are nsys-ui
# and ncu-ui (see issue #2654).
_NSYS_UI_DIR_BY_ARCH = {
"x64": "host-windows-x64",
"arm64": "host-windows-armv8",
}

_NCU_UI_DIR_BY_ARCH = {
"x64": os.path.join("host", "windows-desktop-win7-x64"),
"arm64": os.path.join("host", "windows-desktop-win10-t23x-a64"),
}


def _installed_product_root(product: str) -> str | None:
"""Return the active Nsight product installation recorded by its MSI."""
Expand Down Expand Up @@ -72,3 +85,21 @@ def ncu_candidate_paths() -> Iterator[str]:

target_dir = _NCU_TARGET_DIR_BY_ARCH[windows_machine_arch()]
yield os.path.join(install_root, target_dir, "ncu.exe")


def nsys_ui_candidate_paths() -> Iterator[str]:
install_root = _installed_product_root("Systems")
if install_root is None:
return

host_dir = _NSYS_UI_DIR_BY_ARCH[windows_machine_arch()]
yield os.path.join(install_root, host_dir, "nsys-ui.exe")


def ncu_ui_candidate_paths() -> Iterator[str]:
install_root = _installed_product_root("Compute")
if install_root is None:
return

host_dir = _NCU_UI_DIR_BY_ARCH[windows_machine_arch()]
yield os.path.join(install_root, host_dir, "ncu-ui.exe")
86 changes: 84 additions & 2 deletions cuda_pathfinder/tests/test_find_nvidia_binaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ def test_supported_binaries_consistency():
assert set(SITE_PACKAGES_BINDIRS).issubset(SUPPORTED_BINARIES_ALL)


def test_nsight_gui_utility_names_are_supported():
# Current standalone Nsight releases ship the GUI as nsys-ui/ncu-ui, so
# find_nvidia_binary_utility must not raise UnsupportedBinaryError for them.
assert "nsys-ui" in SUPPORTED_BINARIES
assert "ncu-ui" in SUPPORTED_BINARIES
# The legacy CUDA-toolkit-bundled names remain accepted for compatibility.
assert "nsight-sys" in SUPPORTED_BINARIES
assert "nsight-compute" in SUPPORTED_BINARIES


@pytest.fixture
def clear_find_binary_cache():
find_nvidia_binary_utility.cache_clear()
Expand Down Expand Up @@ -188,7 +198,9 @@ def test_find_compute_sanitizer_uses_canary_ctk_root(monkeypatch, mocker):
("utility_name", "candidate_names"),
(
("nsys", ("nsys.exe",)),
("nsys-ui", ("nsys-ui.exe",)),
("ncu", ("ncu.bat", "ncu.exe")),
("ncu-ui", ("ncu-ui.exe",)),
),
)
@pytest.mark.usefixtures("clear_find_binary_cache")
Expand All @@ -202,7 +214,10 @@ def test_find_binary_windows_nsight_conda_precedes_registry(monkeypatch, mocker,
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)
candidate_paths = mocker.patch.object(binary_finder_module.windows_nsight, f"{utility_name}_candidate_paths")
# Hyphenated utility names map to underscored candidate-path helpers
# (e.g. ``nsys-ui`` -> ``nsys_ui_candidate_paths``).
helper_name = f"{utility_name.replace('-', '_')}_candidate_paths"
candidate_paths = mocker.patch.object(binary_finder_module.windows_nsight, helper_name)
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])
Expand All @@ -222,6 +237,20 @@ def test_find_binary_windows_nsight_conda_precedes_registry(monkeypatch, mocker,
(
("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",)),
(
"nsys-ui",
"Systems",
"x64",
os.path.join("host-windows-x64", "nsys-ui.exe"),
("nsys-ui.exe",),
),
(
"nsys-ui",
"Systems",
"arm64",
os.path.join("host-windows-armv8", "nsys-ui.exe"),
("nsys-ui.exe",),
),
(
"ncu",
"Compute",
Expand All @@ -236,6 +265,20 @@ def test_find_binary_windows_nsight_conda_precedes_registry(monkeypatch, mocker,
os.path.join("target", "windows-desktop-win10-t23x-a64", "ncu.exe"),
("ncu.bat", "ncu.exe"),
),
(
"ncu-ui",
"Compute",
"x64",
os.path.join("host", "windows-desktop-win7-x64", "ncu-ui.exe"),
("ncu-ui.exe",),
),
(
"ncu-ui",
"Compute",
"arm64",
os.path.join("host", "windows-desktop-win10-t23x-a64", "ncu-ui.exe"),
("ncu-ui.exe",),
),
),
)
@pytest.mark.usefixtures("clear_find_binary_cache")
Expand Down Expand Up @@ -299,7 +342,15 @@ def test_find_binary_windows_ncu_launcher_hit_does_not_resolve_machine_arch(monk
canary.assert_not_called()


@pytest.mark.parametrize(("utility_name", "product"), (("nsys", "Systems"), ("ncu", "Compute")))
@pytest.mark.parametrize(
("utility_name", "product"),
(
("nsys", "Systems"),
("nsys-ui", "Systems"),
("ncu", "Compute"),
("ncu-ui", "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):
Expand Down Expand Up @@ -351,6 +402,37 @@ def test_find_windows_nsight_legacy_names_remain_literal_in_early_search(monkeyp
ncu_candidates.assert_not_called()


@pytest.mark.parametrize("utility_name", ("nsys-ui", "ncu-ui"))
@pytest.mark.usefixtures("clear_find_binary_cache")
@pytest.mark.agent_authored(model="deepseek-v4-flash")
def test_find_windows_nsight_gui_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_ui_candidates = mocker.patch.object(binary_finder_module.windows_nsight, "nsys_ui_candidate_paths")
ncu_ui_candidates = mocker.patch.object(binary_finder_module.windows_nsight, "ncu_ui_candidate_paths")
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_ui_candidates.assert_not_called()
ncu_ui_candidates.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")
Expand Down
34 changes: 34 additions & 0 deletions cuda_pathfinder/tests/test_windows_nsight.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,40 @@ def test_ncu_candidate_paths_fall_back_to_machine_binary(mocker, machine_arch, t
assert tuple(windows_nsight.ncu_candidate_paths()) == (launcher, expected)


@pytest.mark.parametrize(
("machine_arch", "host_dir"),
(
("x64", "host-windows-x64"),
("arm64", "host-windows-armv8"),
),
)
@pytest.mark.agent_authored(model="deepseek-v4-flash")
def test_nsys_ui_candidate_paths_use_machine_arch(mocker, machine_arch, host_dir):
install_root = os.path.join(os.sep, "Program Files", "Nsight Systems")
expected = os.path.join(install_root, host_dir, "nsys-ui.exe")
mocker.patch.object(windows_nsight, "_installed_product_root", return_value=install_root)
mocker.patch.object(windows_nsight, "windows_machine_arch", return_value=machine_arch)

assert tuple(windows_nsight.nsys_ui_candidate_paths()) == (expected,)


@pytest.mark.parametrize(
("machine_arch", "host_dir"),
(
("x64", os.path.join("host", "windows-desktop-win7-x64")),
("arm64", os.path.join("host", "windows-desktop-win10-t23x-a64")),
),
)
@pytest.mark.agent_authored(model="deepseek-v4-flash")
def test_ncu_ui_candidate_paths_use_machine_arch(mocker, machine_arch, host_dir):
install_root = os.path.join(os.sep, "Program Files", "Nsight Compute")
expected = os.path.join(install_root, host_dir, "ncu-ui.exe")
mocker.patch.object(windows_nsight, "_installed_product_root", return_value=install_root)
mocker.patch.object(windows_nsight, "windows_machine_arch", return_value=machine_arch)

assert tuple(windows_nsight.ncu_ui_candidate_paths()) == (expected,)


@pytest.mark.agent_authored(model="gpt-5.6")
def test_installed_product_root_reads_64_bit_registry(mocker):
install_root = os.path.join(os.sep, "Program Files", "Nsight Systems")
Expand Down