Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d174f5a
find_nvidia_headers.py initial version (untested).
rwgk May 27, 2025
5ed86c5
Add tests/test_path_finder_find_headers.py, with hard-coded paths.
rwgk May 28, 2025
a98de70
Better error message: UNKNOWN libname='unknown-libname'
rwgk May 28, 2025
6a93fe7
if libname == "nvshmem" and IS_WINDOWS: return None
rwgk May 28, 2025
3ab9643
Merge branch 'main' into find_nvidia_headers_nvshmem and move/adjust/…
rwgk Aug 12, 2025
c1e9385
Move find_nvidia_headers.py → _headers/find_nvidia_headers.py
rwgk Aug 12, 2025
c3464b3
test_find_nvidia_headers.py: removed hard-wired paths, comments with …
rwgk Aug 12, 2025
627f6d0
Merge branch 'main' into find_nvidia_headers_nvshmem
rwgk Aug 22, 2025
26f94d1
Make _find_nvidia_header_directory private for now.
rwgk Aug 22, 2025
6adddb0
test_find_nvidia_headers.py: Move comments with installation commands…
rwgk Aug 22, 2025
363b649
Add `have_nvidia_nvshmem_package()` function to enable `assert hdr_di…
rwgk Aug 22, 2025
2dd448c
Merge branch 'main' into find_nvidia_headers_nvshmem
rwgk Aug 22, 2025
d3f97e4
Add nvidia-nvshmem-cu12,13 in pyproject.toml
rwgk Aug 22, 2025
6f4d762
assert site-packages or dist-packages
rwgk Aug 22, 2025
dfa3384
Add CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS
rwgk Aug 22, 2025
673c38c
Transfer `ci/`, `.github/` changes from PR #864
rwgk Aug 22, 2025
80cece3
Add CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS in `ci/`, `.g…
rwgk Aug 22, 2025
7c30292
reverse=True in sorting of "/usr/include/nvshmem_*" (find newest first)
rwgk Aug 22, 2025
a300419
Fix: assert site-packages or dist-packages only if have_nvidia_nvshme…
rwgk Aug 22, 2025
3ae15e0
pytest.skip("nvshmem has no Windows support.")
rwgk Aug 22, 2025
e855155
Merge branch 'main' into find_nvidia_headers_nvshmem
rwgk Sep 2, 2025
dc4de43
Merge branch 'main' into find_nvidia_headers_nvshmem
rwgk Sep 3, 2025
eb2e78a
Add new cuda/pathfinder/_utils/conda_env.py and use from find_nvidia_…
rwgk Sep 3, 2025
c90c393
Add new cuda/pathfinder/_utils/env_vars_for_include.py and use from f…
rwgk Sep 4, 2025
cee717a
Revert "Add new cuda/pathfinder/_utils/env_vars_for_include.py and us…
rwgk Sep 5, 2025
a50da30
Revert "Add new cuda/pathfinder/_utils/conda_env.py and use from find…
rwgk Sep 5, 2025
a185f03
Merge branch 'main' into find_nvidia_headers_nvshmem
rwgk Sep 5, 2025
7cfcbfe
Bump pathfinder version to 1.2.2 and add release/1.2.2-notes.rst
rwgk Sep 5, 2025
510f470
Remove os.path.isdir() tests that are not strictly needed.
rwgk Sep 5, 2025
2426260
test_find_nvidia_headers.py: remove check for `dist-packages` because…
rwgk Sep 5, 2025
b74a84c
Additional testing
rwgk Sep 5, 2025
6ee6529
Merge branch 'main' into find_nvidia_headers_nvshmem
rwgk Sep 5, 2025
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
2 changes: 2 additions & 0 deletions .github/workflows/test-wheel-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ jobs:
- name: Run cuda.pathfinder tests with see_what_works
env:
CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works
CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works
run: run-tests pathfinder

- name: Run cuda.bindings tests
Expand Down Expand Up @@ -332,4 +333,5 @@ jobs:
- name: Run cuda.pathfinder tests with all_must_work
env:
CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work
CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work
run: run-tests pathfinder
2 changes: 2 additions & 0 deletions .github/workflows/test-wheel-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ jobs:
- name: Run cuda.pathfinder tests with see_what_works
env:
CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works
CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: see_what_works
shell: bash --noprofile --norc -xeuo pipefail {0}
run: run-tests pathfinder

Expand Down Expand Up @@ -299,5 +300,6 @@ jobs:
- name: Run cuda.pathfinder tests with all_must_work
env:
CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work
CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS: all_must_work
shell: bash --noprofile --norc -xeuo pipefail {0}
run: run-tests pathfinder
4 changes: 3 additions & 1 deletion ci/tools/run-tests
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ popd

if [[ "${test_module}" == "pathfinder" ]]; then
pushd ./cuda_pathfinder
echo "Running pathfinder tests with ${CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS}"
echo "Running pathfinder tests with " \
"LD:${CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS} " \
"FH:${CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS}"
pwd
pytest -ra -s -v tests/
popd
Expand Down
3 changes: 3 additions & 0 deletions cuda_pathfinder/cuda/pathfinder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import (
SUPPORTED_LIBNAMES as SUPPORTED_NVIDIA_LIBNAMES, # noqa: F401
)
from cuda.pathfinder._headers.find_nvidia_headers import (
find_nvidia_header_directory as _find_nvidia_header_directory, # noqa: F401
)
from cuda.pathfinder._version import __version__ as __version__
52 changes: 52 additions & 0 deletions cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import functools
import glob
import os
from typing import Optional, cast

from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import IS_WINDOWS
from cuda.pathfinder._utils.conda_env import get_conda_prefix
from cuda.pathfinder._utils.env_vars_for_include import iter_env_vars_for_include_dirs
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages


@functools.cache
def find_nvidia_header_directory(libname: str) -> Optional[str]:
if libname != "nvshmem":
raise RuntimeError(f"UNKNOWN {libname=}")

if libname == "nvshmem" and IS_WINDOWS:
# nvshmem has no Windows support.
return None

# Installed from a wheel
nvidia_sub_dirs = ("nvidia", "nvshmem", "include")
hdr_dir: str # help mypy
for hdr_dir in find_sub_dirs_all_sitepackages(nvidia_sub_dirs):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should do it in a follow up, but I think we should really move towards using an importlib based resolution method instead of walking the sitepackages ourselves.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I thought we discussed and agreed walking the paths is acceptable since on the PyPI side they are predictable? Any reason to prefer importlib?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a good point, tx, I created issue #949 to track this.

(I wrote this code before @ZzEeKkAa pointed me to importlib while working on PR #864)

Copy link
Copy Markdown
Contributor Author

@rwgk rwgk Sep 5, 2025

Choose a reason for hiding this comment

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

I thought we discussed and agreed walking the paths is acceptable since on the PyPI side they are predictable? Any reason to prefer importlib?

Lines crossed here. Let's review under #949 when we get to it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

864?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oops, sorry, copy-paste mishap. Corrected (949).

nvshmem_h_path = os.path.join(hdr_dir, "nvshmem.h")
if os.path.isfile(nvshmem_h_path):
return hdr_dir

conda_prefix = get_conda_prefix()
if conda_prefix and os.path.isdir(conda_prefix.path):
hdr_dir = os.path.join(conda_prefix.path, "include")
if os.path.isdir(hdr_dir):
Comment thread
rwgk marked this conversation as resolved.
Outdated
nvshmem_h_path = os.path.join(hdr_dir, "nvshmem.h")
if os.path.isfile(nvshmem_h_path):
return hdr_dir

for hdr_dir in sorted(glob.glob("/usr/include/nvshmem_*"), reverse=True):
Comment thread
leofang marked this conversation as resolved.
if os.path.isdir(hdr_dir):
nvshmem_h_path = os.path.join(hdr_dir, "nvshmem.h")
if os.path.isfile(nvshmem_h_path):
return hdr_dir

for hdr_dir in iter_env_vars_for_include_dirs():
if os.path.isdir(hdr_dir):
nvshmem_h_path = os.path.join(hdr_dir, "nvshmem.h")
if os.path.isfile(nvshmem_h_path):
return cast(str, hdr_dir) # help mypy

return None
Comment thread
rwgk marked this conversation as resolved.
41 changes: 41 additions & 0 deletions cuda_pathfinder/cuda/pathfinder/_utils/conda_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import functools
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Literal, Optional

# https://docs.conda.io/projects/conda-build/en/stable/user-guide/environment-variables.html

BUILD_STATES = ("RENDER", "BUILD", "TEST")
Comment thread
rwgk marked this conversation as resolved.
Outdated


@dataclass(frozen=True)
class CondaPrefix:
env_state: Literal["RENDER", "BUILD", "TEST", "activated"]
Comment thread
rwgk marked this conversation as resolved.
Outdated
path: Path


@functools.cache
def get_conda_prefix() -> Optional[CondaPrefix]:
"""
Return the effective conda prefix.
- RENDER, BUILD, TEST: inside conda-build (host prefix at $PREFIX)
Comment thread
rwgk marked this conversation as resolved.
Outdated
- activated: user-activated env ($CONDA_PREFIX)
- None: neither detected
"""
state = os.getenv("CONDA_BUILD_STATE")
if state:
if state in BUILD_STATES:
p = os.getenv("PREFIX")
Comment thread
rwgk marked this conversation as resolved.
Outdated
if p:
return CondaPrefix(state, Path(p)) # type: ignore[arg-type]
return None

cp = os.getenv("CONDA_PREFIX")
if cp:
return CondaPrefix("activated", Path(cp))

return None
25 changes: 25 additions & 0 deletions cuda_pathfinder/cuda/pathfinder/_utils/env_vars_for_include.py
Comment thread
rwgk marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import os
import sys
from collections.abc import Iterable

IS_WINDOWS = sys.platform == "win32"

# GCC/Clang-style include vars
GCC_VNAMES = ("CPATH", "C_INCLUDE_PATH", "CPLUS_INCLUDE_PATH")

# MSVC: INCLUDE is the canonical header search variable
MSVC_GCC_VNAMES = ("INCLUDE",)

VNAMES: tuple[str, ...] = MSVC_GCC_VNAMES + GCC_VNAMES if IS_WINDOWS else GCC_VNAMES


def iter_env_vars_for_include_dirs() -> Iterable[str]:
for vname in VNAMES:
v = os.getenv(vname)
if v:
for d in v.split(os.pathsep):
if d:
yield d
59 changes: 59 additions & 0 deletions cuda_pathfinder/tests/test_find_nvidia_headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Currently these installations are only manually tested:

# conda create -y -n nvshmem python=3.12
# conda activate nvshmem
# conda install -y conda-forge::libnvshmem3 conda-forge::libnvshmem-dev

# wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
# sudo dpkg -i cuda-keyring_1.1-1_all.deb
# sudo apt update
# sudo apt install libnvshmem3-cuda-12 libnvshmem3-dev-cuda-12
# sudo apt install libnvshmem3-cuda-13 libnvshmem3-dev-cuda-13

import functools
import importlib.metadata
import os
import re

import pytest

from cuda.pathfinder import _find_nvidia_header_directory as find_nvidia_header_directory
from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import IS_WINDOWS

STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_FIND_NVIDIA_HEADERS_STRICTNESS", "see_what_works")
assert STRICTNESS in ("see_what_works", "all_must_work")


@functools.cache
def have_nvidia_nvshmem_package() -> bool:
pattern = re.compile(r"^nvidia-nvshmem-.*$")
return any(
pattern.match(dist.metadata["Name"]) for dist in importlib.metadata.distributions() if "Name" in dist.metadata
)


def test_unknown_libname():
with pytest.raises(RuntimeError, match=r"^UNKNOWN libname='unknown-libname'$"):
find_nvidia_header_directory("unknown-libname")


def test_find_libname_nvshmem(info_summary_append):
hdr_dir = find_nvidia_header_directory("nvshmem")
info_summary_append(f"{hdr_dir=!r}")
if IS_WINDOWS:
assert hdr_dir is None
pytest.skip("nvshmem has no Windows support.")
if STRICTNESS == "all_must_work" or have_nvidia_nvshmem_package():
assert hdr_dir is not None
if have_nvidia_nvshmem_package():
hdr_dir_parts = hdr_dir.split(os.path.sep)
assert any(
sub_dir in hdr_dir_parts
for sub_dir in (
"site-packages", # pip install
"dist-packages", # apt install
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need to do anything for conda packages here?

Copy link
Copy Markdown
Contributor Author

@rwgk rwgk Sep 5, 2025

Choose a reason for hiding this comment

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

This is gated by have_nvidia_nvshmem_package(). ... But I just realized, there is no nvshmem .deb that installs into dist-packages. So I removed dist-packages from here to not implicitly suggest that it exists: commit 2426260

While at it, I added additional test (low-hanging fruits): commit b74a84c

Interactive testing with conda and "all_must_work" set:

$ pytest -ra -s -vv tests/test_find_nvidia_headers.py
========================================================================= test session starts ==========================================================================
platform linux -- Python 3.12.11, pytest-8.4.1, pluggy-1.6.0 -- /home/rgrossekunst/miniforge3/envs/nvshmem/bin/python3.12
cachedir: .pytest_cache
rootdir: /home/rgrossekunst/forked/cuda-python/cuda_pathfinder
configfile: pyproject.toml
collected 2 items

tests/test_find_nvidia_headers.py::test_unknown_libname PASSED
tests/test_find_nvidia_headers.py::test_find_libname_nvshmem PASSED

============================================================================= INFO summary =============================================================================
INFO test_find_libname_nvshmem: hdr_dir='/home/rgrossekunst/miniforge3/envs/nvshmem/include'
========================================================================== 2 passed in 0.01s ===========================================================================

Without wheel or conda ("all_must_work" set):

$ pytest -ra -s -vv tests/test_find_nvidia_headers.py
========================================================================= test session starts ==========================================================================
platform linux -- Python 3.12.3, pytest-8.4.2, pluggy-1.6.0 -- /home/rgrossekunst/JunkVenv/bin/python3
cachedir: .pytest_cache
rootdir: /home/rgrossekunst/forked/cuda-python/cuda_pathfinder
configfile: pyproject.toml
collected 2 items

tests/test_find_nvidia_headers.py::test_unknown_libname PASSED
tests/test_find_nvidia_headers.py::test_find_libname_nvshmem PASSED

============================================================================= INFO summary =============================================================================
INFO test_find_libname_nvshmem: hdr_dir='/usr/include/nvshmem_13'
========================================================================== 2 passed in 0.02s ===========================================================================

I also did negative tests for both, by using bad expected paths, and they fail as expected.

)
), hdr_dir
105 changes: 105 additions & 0 deletions cuda_pathfinder/tests/test_utils_conda_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import pytest

from cuda.pathfinder._utils.conda_env import (
BUILD_STATES,
CondaPrefix,
get_conda_prefix,
)

# Auto-clean environment & cache before every test -----------------------------


@pytest.fixture(autouse=True)
def _clean_env_and_cache(monkeypatch):
# Remove any possibly inherited variables from the test runner environment
for k in ("CONDA_BUILD_STATE", "PREFIX", "CONDA_PREFIX"):
monkeypatch.delenv(k, raising=False)
# Clear the cached result between tests
get_conda_prefix.cache_clear()
return
# (No teardown needed; monkeypatch auto-reverts)


# Tests -----------------------------------------------------------------------


def test_returns_none_when_no_relevant_env_vars():
assert get_conda_prefix() is None


@pytest.mark.parametrize("state", BUILD_STATES)
def test_build_state_returns_prefix_when_present(state, monkeypatch, tmp_path):
monkeypatch.setenv("CONDA_BUILD_STATE", state)
monkeypatch.setenv("PREFIX", str(tmp_path))
res = get_conda_prefix()
assert isinstance(res, CondaPrefix)
assert res.env_state == state
assert res.path == tmp_path


@pytest.mark.parametrize("state", BUILD_STATES)
def test_build_state_requires_prefix_otherwise_none(state, monkeypatch):
monkeypatch.setenv("CONDA_BUILD_STATE", state)
# No PREFIX set
assert get_conda_prefix() is None


@pytest.mark.parametrize("state", BUILD_STATES)
def test_build_state_with_empty_prefix_returns_none(state, monkeypatch):
monkeypatch.setenv("CONDA_BUILD_STATE", state)
monkeypatch.setenv("PREFIX", "")
assert get_conda_prefix() is None


def test_activated_env_returns_conda_prefix(monkeypatch, tmp_path):
monkeypatch.setenv("CONDA_PREFIX", str(tmp_path))
res = get_conda_prefix()
assert isinstance(res, CondaPrefix)
assert res.env_state == "activated"
assert res.path == tmp_path


def test_activated_env_ignores_empty_conda_prefix(monkeypatch):
monkeypatch.setenv("CONDA_PREFIX", "")
assert get_conda_prefix() is None


def test_build_state_wins_over_activated_when_valid(monkeypatch, tmp_path):
build_p = tmp_path / "host"
user_p = tmp_path / "user"
monkeypatch.setenv("CONDA_BUILD_STATE", "TEST")
monkeypatch.setenv("PREFIX", str(build_p))
monkeypatch.setenv("CONDA_PREFIX", str(user_p))
res = get_conda_prefix()
assert res
assert res.env_state == "TEST"
assert res.path == build_p


def test_unknown_build_state_returns_none_even_if_conda_prefix_set(monkeypatch, tmp_path):
# Any non-empty CONDA_BUILD_STATE that is not recognized -> None
monkeypatch.setenv("CONDA_BUILD_STATE", "SOMETHING_ELSE")
monkeypatch.setenv("CONDA_PREFIX", str(tmp_path))
assert get_conda_prefix() is None


def test_empty_build_state_treated_as_absent_and_falls_back_to_activated(monkeypatch, tmp_path):
# Empty string is falsy -> treated like "not set" -> activated path
monkeypatch.setenv("CONDA_BUILD_STATE", "")
monkeypatch.setenv("CONDA_PREFIX", str(tmp_path))
res = get_conda_prefix()
assert res
assert res.env_state == "activated"
assert res.path == tmp_path


def test_have_cache(monkeypatch, tmp_path):
monkeypatch.setenv("CONDA_PREFIX", str(tmp_path))
res = get_conda_prefix()
assert res
assert res.path == tmp_path
res2 = get_conda_prefix()
assert res2 is res
Loading
Loading