diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index f7852155f..474179f81 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -115,6 +115,17 @@ jobs: pwd ls -lahR . + - name: Download cuda-pathfinder build artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: cuda-pathfinder-wheel + path: ./cuda_pathfinder + + - name: Display structure of downloaded cuda-pathfinder artifacts + run: | + pwd + ls -lahR cuda_pathfinder + - name: Download cuda.bindings build artifacts if: ${{ !inputs.is-release }} uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -161,6 +172,10 @@ jobs: - name: Install all packages run: | + pushd cuda_pathfinder + pip install *.whl + popd + pushd "${CUDA_BINDINGS_ARTIFACTS_DIR}" pip install *.whl popd diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index c8a163c97..2e8d99f4e 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -57,7 +57,7 @@ jobs: - name: Set up MSVC if: ${{ startsWith(inputs.host-platform, 'win') }} uses: ilammy/msvc-dev-cmd@v1 # TODO: ask admin to allow pinning commits - + - name: Set environment variables env: CUDA_VER: ${{ inputs.cuda-version }} @@ -69,7 +69,43 @@ jobs: - name: Dump environment run: | env - + + - name: Install twine + run: | + pip install twine + + # To keep the build workflow simple, all matrix jobs will build a wheel for later use within this workflow. + - name: Build and check cuda.pathfinder wheel + run: | + pushd cuda_pathfinder + pip wheel -v --no-deps . + popd + + - name: List the cuda.pathfinder artifacts directory + run: | + if [[ "${{ inputs.host-platform }}" == win* ]]; then + export CHOWN=chown + else + export CHOWN="sudo chown" + fi + $CHOWN -R $(whoami) cuda_pathfinder/*.whl + ls -lahR cuda_pathfinder + + # We only need/want a single pure python wheel, pick linux-64 index 0. + # This is what we will use for testing & releasing. + - name: Check cuda.pathfinder wheel + if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + run: | + twine check cuda_pathfinder/*.whl + + - name: Upload cuda.pathfinder build artifacts + if: ${{ strategy.job-index == 0 && inputs.host-platform == 'linux-64' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: cuda-pathfinder-wheel + path: cuda_pathfinder/*.whl + if-no-files-found: error + - name: Build cuda.core wheel uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0 env: @@ -96,7 +132,6 @@ jobs: - name: Check cuda.core wheel run: | - pip install twine twine check ${{ env.CUDA_CORE_ARTIFACTS_DIR }}/*.whl - name: Upload cuda.core build artifacts @@ -200,6 +235,10 @@ jobs: # For caching echo "PY_EXT_SUFFIX=$(python -c "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")" >> $GITHUB_ENV + - name: Install cuda.pathfinder (required for next step) + run: | + pip install cuda_pathfinder/*.whl + - name: Build cuda.bindings Cython tests run: | pip install $(ls ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}/*.whl)[test] diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 59c1ae43b..ef6b966e8 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -163,6 +163,12 @@ jobs: SHA: ${{ github.sha }} run: ./ci/tools/env-vars test + - name: Download cuda-pathfinder build artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: cuda-pathfinder-wheel + path: ./cuda_pathfinder + - name: Download cuda-python build artifacts if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -286,6 +292,11 @@ jobs: - name: Set up compute-sanitizer run: setup-sanitizer + - name: Run cuda.pathfinder tests with see_what_works + env: + CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works + run: run-tests pathfinder + - name: Run cuda.bindings tests if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0' }} env: @@ -306,3 +317,17 @@ jobs: else pip install $(ls cuda_python*.whl)[all] fi + + - name: Install cuda.pathfinder nvidia_wheels_cu12 + if: startsWith(matrix.CUDA_VER, '12.') + run: | + pushd cuda_pathfinder + pip install -v .[nvidia_wheels_cu12] + pip freeze + popd + + - name: Run cuda.pathfinder tests with all_must_work + if: startsWith(matrix.CUDA_VER, '12.') + env: + CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work + run: run-tests pathfinder diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index f5c6db6fc..b3b51e0fd 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -121,6 +121,12 @@ jobs: shell: bash --noprofile --norc -xeuo pipefail {0} run: ./ci/tools/env-vars test + - name: Download cuda-pathfinder build artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: cuda-pathfinder-wheel + path: ./cuda_pathfinder + - name: Download cuda-python build artifacts if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 @@ -184,7 +190,7 @@ jobs: New-Item -Path "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" -ItemType Directory -Force Move-Item -Path "$OLD_BASENAME/*.whl" -Destination "${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}" Remove-Item -Path $OLD_BASENAME -Force - + gh run download $LATEST_PRIOR_RUN_ID -p cuda-python-wheel -R NVIDIA/cuda-python Get-ChildItem -Path cuda-python-wheel Move-Item -Path "cuda-python-wheel/*.whl" -Destination . @@ -250,6 +256,12 @@ jobs: host-platform: ${{ inputs.host-platform }} cuda-version: ${{ matrix.CUDA_VER }} + - name: Run cuda.pathfinder tests with see_what_works + env: + CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: see_what_works + shell: bash --noprofile --norc -xeuo pipefail {0} + run: run-tests pathfinder + - name: Run cuda.bindings tests if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0' }} env: @@ -272,3 +284,19 @@ jobs: } else { pip install "$((Get-ChildItem -Filter cuda_python*.whl).FullName)[all]" } + + - name: Install cuda.pathfinder nvidia_wheels_cu12 + if: startsWith(matrix.CUDA_VER, '12.') + shell: bash --noprofile --norc -xeuo pipefail {0} + run: | + pushd cuda_pathfinder + pip install -v .[nvidia_wheels_cu12] + pip freeze + popd + + - name: Run cuda.pathfinder tests with all_must_work + if: startsWith(matrix.CUDA_VER, '12.') + env: + CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS: all_must_work + shell: bash --noprofile --norc -xeuo pipefail {0} + run: run-tests pathfinder diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a7112246a..7e9059d22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,5 +37,13 @@ repos: - --ini - .bandit + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 0f86793af5ef5f6dc63c8d04a3cabfa3ea8f9c6a # frozen: v1.16.1 + hooks: + - id: mypy + name: mypy-pathfinder + files: ^cuda_pathfinder/cuda/.*\.py$ # Exclude tests directory + args: [--config-file=cuda_pathfinder/mypy.ini] + default_language_version: python: python3 diff --git a/ci/tools/run-tests b/ci/tools/run-tests index 3a35b6ad2..363f7ccef 100755 --- a/ci/tools/run-tests +++ b/ci/tools/run-tests @@ -9,15 +9,33 @@ set -euo pipefail # Check if the script was called with exactly 1 argument -if [[ ${#} -ne 1 && ( "${1}" == "bindings" || "${1}" == "core" ) ]]; then - echo "Error: This script requires exactly 1 argument (what is tested). You provided ${#}" - echo "Usage: ${0} test_module[bindings or core]" +if [[ ${#} -ne 1 ]]; then + echo "Error: This script requires exactly 1 argument. You provided ${#}" + exit 1 +fi +if [[ "${1}" != "bindings" && "${1}" != "core" && "${1}" != "pathfinder" ]]; then + echo "Error: Invalid test module '${1}'. Must be 'bindings', 'core', or 'pathfinder'" exit 1 fi test_module=${1} -if [[ "${test_module}" == "bindings" ]]; then +# Unconditionally install pathfinder wheel +# (it is a direct dependency of bindings, and a transitive dependency of core) +pushd ./cuda_pathfinder +echo "Installing pathfinder wheel" +pwd +ls +pip install $(ls *.whl)[test] +popd + +if [[ "${test_module}" == "pathfinder" ]]; then + pushd ./cuda_pathfinder + echo "Running pathfinder tests with ${CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS}" + pwd + pytest -ra -s -v tests/ + popd +elif [[ "${test_module}" == "bindings" ]]; then pushd "${CUDA_BINDINGS_ARTIFACTS_DIR}" echo "Installing bindings wheel" pwd diff --git a/cuda_bindings/cuda/bindings/_path_finder/README.md b/cuda_bindings/cuda/bindings/_path_finder/README.md index 1e115f2ed..d402c935c 100644 --- a/cuda_bindings/cuda/bindings/_path_finder/README.md +++ b/cuda_bindings/cuda/bindings/_path_finder/README.md @@ -1,56 +1 @@ -# `cuda.bindings.path_finder` Module - -## Public API (Work in Progress) - -Currently exposes two primary interfaces: - -``` -cuda.bindings.path_finder._SUPPORTED_LIBNAMES # ('nvJitLink', 'nvrtc', 'nvvm') -cuda.bindings.path_finder._load_nvidia_dynamic_library(libname: str) -> LoadedDL -``` - -**Note:** -These APIs are prefixed with an underscore because they are considered -experimental while undergoing active development, although already -reasonably well-tested through CI pipelines. - -## Library Loading Search Priority - -The `load_nvidia_dynamic_library()` function implements a hierarchical search -strategy for locating NVIDIA shared libraries: - -0. **Check if a library was loaded into the process already by some other means.** - - If yes, there is no alternative to skipping the rest of the search logic. - The absolute path of the already loaded library will be returned, along - with the handle to the library. - -1. **NVIDIA Python wheels** - - Scans all site-packages to find libraries installed via NVIDIA Python wheels. - -2. **OS default mechanisms / Conda environments** - - Falls back to native loader: - - `dlopen()` on Linux - - `LoadLibraryW()` on Windows - - Conda installations are expected to be discovered: - - Linux: Via `$ORIGIN/../lib` on `RPATH` (of the `python` binary; - note that this preempts `LD_LIBRARY_PATH` and `/etc/ld.so.conf.d/`) - - Windows: Via `%CONDA_PREFIX%\Library\bin` on system `PATH` - - CTK installations with system config updates are expected to be discovered: - - Linux: Via `/etc/ld.so.conf.d/*cuda*.conf` - - Windows: Via `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vX.Y\bin` on system `PATH` - -3. **Environment variables** - - Relies on `CUDA_HOME` or `CUDA_PATH` environment variables if set - (in that order). - -Note that the search is done on a per-library basis. There is no centralized -mechanism that ensures all libraries are found in the same way. - -## Maintenance Requirements - -These key components must be updated for new CUDA Toolkit releases: - -- `supported_libs.SUPPORTED_LIBNAMES` -- `supported_libs.SUPPORTED_WINDOWS_DLLS` -- `supported_libs.SUPPORTED_LINUX_SONAMES` -- `supported_libs.EXPECTED_LIB_SYMBOLS` +# The `cuda.bindings.path_finder` module was moved → `cuda.pathfinder` diff --git a/cuda_bindings/cuda/bindings/_path_finder/load_dl_common.py b/cuda_bindings/cuda/bindings/_path_finder/load_dl_common.py deleted file mode 100644 index 034b9d433..000000000 --- a/cuda_bindings/cuda/bindings/_path_finder/load_dl_common.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -from dataclasses import dataclass -from typing import Callable, Optional - -from cuda.bindings._path_finder.supported_libs import DIRECT_DEPENDENCIES, IS_WINDOWS - -if IS_WINDOWS: - import pywintypes - - HandleType = pywintypes.HANDLE -else: - HandleType = int - - -@dataclass -class LoadedDL: - handle: HandleType - abs_path: Optional[str] - was_already_loaded_from_elsewhere: bool - - -def load_dependencies(libname: str, load_func: Callable[[str], LoadedDL]) -> None: - """Load all dependencies for a given library. - - Args: - libname: The name of the library whose dependencies should be loaded - load_func: The function to use for loading libraries (e.g. load_nvidia_dynamic_library) - - Example: - >>> load_dependencies("cudart", load_nvidia_dynamic_library) - # This will load all dependencies of cudart using the provided loading function - """ - for dep in DIRECT_DEPENDENCIES.get(libname, ()): - load_func(dep) diff --git a/cuda_bindings/cuda/bindings/path_finder.py b/cuda_bindings/cuda/bindings/path_finder.py index 28badd025..da129e6b4 100644 --- a/cuda_bindings/cuda/bindings/path_finder.py +++ b/cuda_bindings/cuda/bindings/path_finder.py @@ -1,11 +1,8 @@ # Copyright 2024-2025 NVIDIA Corporation. All rights reserved. -# # SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE -from cuda.bindings._path_finder.load_nvidia_dynamic_library import ( - load_nvidia_dynamic_library as _load_nvidia_dynamic_library, -) -from cuda.bindings._path_finder.supported_libs import SUPPORTED_LIBNAMES as _SUPPORTED_LIBNAMES +from cuda.pathfinder import SUPPORTED_NVIDIA_LIBNAMES as _SUPPORTED_LIBNAMES +from cuda.pathfinder import load_nvidia_dynamic_lib as _load_nvidia_dynamic_library __all__ = [ "_load_nvidia_dynamic_library", diff --git a/cuda_bindings/pyproject.toml b/cuda_bindings/pyproject.toml index 42ea4bd96..47cc6a00e 100644 --- a/cuda_bindings/pyproject.toml +++ b/cuda_bindings/pyproject.toml @@ -27,6 +27,7 @@ dynamic = [ "readme", ] dependencies = [ + "cuda-pathfinder ~= 1.0", "pywin32; sys_platform == 'win32'", ] diff --git a/cuda_bindings/tests/test_path_finder_load.py b/cuda_bindings/tests/test_path_finder_load.py deleted file mode 100644 index c63c3d668..000000000 --- a/cuda_bindings/tests/test_path_finder_load.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE - -import os -import sys - -import pytest -import spawned_process_runner - -from cuda.bindings import path_finder -from cuda.bindings._path_finder import supported_libs - -ALL_LIBNAMES = path_finder._SUPPORTED_LIBNAMES + supported_libs.PARTIALLY_SUPPORTED_LIBNAMES_ALL -ALL_LIBNAMES_LINUX = path_finder._SUPPORTED_LIBNAMES + supported_libs.PARTIALLY_SUPPORTED_LIBNAMES_LINUX -ALL_LIBNAMES_WINDOWS = path_finder._SUPPORTED_LIBNAMES + supported_libs.PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS -if os.environ.get("CUDA_BINDINGS_PATH_FINDER_TEST_ALL_LIBNAMES", False): - if sys.platform == "win32": - TEST_FIND_OR_LOAD_LIBNAMES = ALL_LIBNAMES_WINDOWS - else: - TEST_FIND_OR_LOAD_LIBNAMES = ALL_LIBNAMES_LINUX -else: - TEST_FIND_OR_LOAD_LIBNAMES = path_finder._SUPPORTED_LIBNAMES - - -def test_all_libnames_linux_sonames_consistency(): - assert tuple(sorted(ALL_LIBNAMES_LINUX)) == tuple(sorted(supported_libs.SUPPORTED_LINUX_SONAMES.keys())) - - -def test_all_libnames_windows_dlls_consistency(): - assert tuple(sorted(ALL_LIBNAMES_WINDOWS)) == tuple(sorted(supported_libs.SUPPORTED_WINDOWS_DLLS.keys())) - - -@pytest.mark.parametrize("dict_name", ["SUPPORTED_LINUX_SONAMES", "SUPPORTED_WINDOWS_DLLS"]) -def test_libname_dict_values_are_unique(dict_name): - libname_dict = getattr(supported_libs, dict_name) - libname_for_value = {} - for libname, values in libname_dict.items(): - for value in values: - prev_libname = libname_for_value.get(value) - if prev_libname is not None: - raise RuntimeError(f"Multiple libnames for {value!r}: {prev_libname}, {libname}") - libname_for_value[value] = libname - - -def test_all_libnames_libnames_requiring_os_add_dll_directory_consistency(): - assert not (set(supported_libs.LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY) - set(ALL_LIBNAMES_WINDOWS)) - - -def test_all_libnames_expected_lib_symbols_consistency(): - assert tuple(sorted(ALL_LIBNAMES)) == tuple(sorted(supported_libs.EXPECTED_LIB_SYMBOLS.keys())) - - -def build_child_process_failed_for_libname_message(libname, result): - return ( - f"Child process failed for {libname=!r} with exit code {result.returncode}\n" - f"--- stdout-from-child-process ---\n{result.stdout}\n" - f"--- stderr-from-child-process ---\n{result.stderr}\n" - ) - - -def child_process_func(libname): - import os - - from cuda.bindings._path_finder.load_nvidia_dynamic_library import _load_nvidia_dynamic_library_no_cache - from cuda.bindings.path_finder import _load_nvidia_dynamic_library - - loaded_dl_fresh = _load_nvidia_dynamic_library(libname) - if loaded_dl_fresh.was_already_loaded_from_elsewhere: - raise RuntimeError("loaded_dl_fresh.was_already_loaded_from_elsewhere") - - loaded_dl_from_cache = _load_nvidia_dynamic_library(libname) - if loaded_dl_from_cache is not loaded_dl_fresh: - raise RuntimeError("loaded_dl_from_cache is not loaded_dl_fresh") - - loaded_dl_no_cache = _load_nvidia_dynamic_library_no_cache(libname) - if not loaded_dl_no_cache.was_already_loaded_from_elsewhere: - raise RuntimeError("loaded_dl_no_cache.was_already_loaded_from_elsewhere") - if not os.path.samefile(loaded_dl_no_cache.abs_path, loaded_dl_fresh.abs_path): - raise RuntimeError(f"not os.path.samefile({loaded_dl_no_cache.abs_path=!r}, {loaded_dl_fresh.abs_path=!r})") - - print(f"{loaded_dl_fresh.abs_path!r}") - - -@pytest.mark.parametrize("libname", TEST_FIND_OR_LOAD_LIBNAMES) -def test_find_or_load_nvidia_dynamic_library(info_summary_append, libname): - # We intentionally run each dynamic library operation in a child process - # to ensure isolation of global dynamic linking state (e.g., dlopen handles). - # Without child processes, loading/unloading libraries during testing could - # interfere across test cases and lead to nondeterministic or platform-specific failures. - result = spawned_process_runner.run_in_spawned_child_process(child_process_func, args=(libname,), timeout=30) - if result.returncode == 0: - info_summary_append(f"abs_path={result.stdout.rstrip()}") - else: - raise RuntimeError(build_child_process_failed_for_libname_message(libname, result)) diff --git a/cuda_pathfinder/LICENSE b/cuda_pathfinder/LICENSE new file mode 100644 index 000000000..f433b1a53 --- /dev/null +++ b/cuda_pathfinder/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/cuda_pathfinder/cuda/pathfinder/README.md b/cuda_pathfinder/cuda/pathfinder/README.md new file mode 100644 index 000000000..297b71e8e --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/README.md @@ -0,0 +1,46 @@ +# `cuda.pathfinder` Module + +## Public API for loading NVIDIA Dynamic Libs + +* `cuda.pathfinder.SUPPORTED_NVIDIA_LIBNAMES` (`tuple[str]`) + +* `cuda.pathfinder.load_nvidia_dynamic_lib(libname: str) -> LoadedDL` + +* `cuda.pathfinder.LoadedDL`: + * `handle` (platform-specific type) + * `abs_path` (`str`) + * `was_already_loaded_from_elsewhere` (`bool`) + +* `cuda.pathfinder.DynamicLibNotFound` (inherits from `RuntimeError`) + +## Dynamic Library Loading Search Priority + +The `cuda.pathfinder.load_nvidia_dynamic_lib` function implements a +hierarchical search strategy for locating NVIDIA shared libraries: + +0. **Check if a library was loaded into the process already by some other means.** + - If yes, there is no alternative to skipping the rest of the search logic. + The absolute path of the already loaded library will be returned, along + with the handle to the library. + +1. **NVIDIA Python wheels** + - Scans all site-packages to find libraries installed via NVIDIA Python wheels. + +2. **OS default mechanisms / Conda environments** + - Falls back to native loader: + - `dlopen()` on Linux + - `LoadLibraryW()` on Windows + - Conda installations are expected to be discovered: + - Linux: Via `$ORIGIN/../lib` on `RPATH` (of the `python` binary; + note that this preempts `LD_LIBRARY_PATH` and `/etc/ld.so.conf.d/`) + - Windows: Via `%CONDA_PREFIX%\Library\bin` on system `PATH` + - CTK installations with system config updates are expected to be discovered: + - Linux: Via `/etc/ld.so.conf.d/*cuda*.conf` + - Windows: Via `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vX.Y\bin` on system `PATH` + +3. **Environment variables** + - Relies on `CUDA_HOME` or `CUDA_PATH` environment variables if set + (in that order). + +Note that the search is done on a per-library basis. Currently there is no +centralized mechanism that ensures all libraries are found in the same way. diff --git a/cuda_pathfinder/cuda/pathfinder/__init__.py b/cuda_pathfinder/cuda/pathfinder/__init__.py new file mode 100644 index 000000000..e148036d5 --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/__init__.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from cuda.pathfinder._dynamic_libs.load_dl_common import DynamicLibNotFound as DynamicLibNotFound +from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL as LoadedDL +from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import load_nvidia_dynamic_lib as load_nvidia_dynamic_lib +from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import ( + SUPPORTED_LIBNAMES as SUPPORTED_NVIDIA_LIBNAMES, # noqa: F401 +) +from cuda.pathfinder._version import __version__ as __version__ diff --git a/cuda_bindings/cuda/bindings/_path_finder/find_nvidia_dynamic_library.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/find_nvidia_dynamic_lib.py similarity index 62% rename from cuda_bindings/cuda/bindings/_path_finder/find_nvidia_dynamic_library.py rename to cuda_pathfinder/cuda/pathfinder/_dynamic_libs/find_nvidia_dynamic_lib.py index 9835b72d0..4d0d4f9c9 100644 --- a/cuda_bindings/cuda/bindings/_path_finder/find_nvidia_dynamic_library.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/find_nvidia_dynamic_lib.py @@ -1,15 +1,23 @@ -# Copyright 2024-2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 import functools import glob import os +from collections.abc import Sequence +from typing import Optional -from cuda.bindings._path_finder.find_sub_dirs import find_sub_dirs_all_sitepackages -from cuda.bindings._path_finder.supported_libs import IS_WINDOWS, is_suppressed_dll_file +from cuda.pathfinder._dynamic_libs.load_dl_common import DynamicLibNotFound +from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import ( + IS_WINDOWS, + is_suppressed_dll_file, +) +from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages -def _no_such_file_in_sub_dirs(sub_dirs, file_wild, error_messages, attachments): +def _no_such_file_in_sub_dirs( + sub_dirs: Sequence[str], file_wild: str, error_messages: list[str], attachments: list[str] +) -> None: error_messages.append(f"No such file: {file_wild}") for sub_dir in find_sub_dirs_all_sitepackages(sub_dirs): attachments.append(f' listdir("{sub_dir}"):') @@ -17,7 +25,9 @@ def _no_such_file_in_sub_dirs(sub_dirs, file_wild, error_messages, attachments): attachments.append(f" {node}") -def _find_so_using_nvidia_lib_dirs(libname, so_basename, error_messages, attachments): +def _find_so_using_nvidia_lib_dirs( + libname: str, so_basename: str, error_messages: list[str], attachments: list[str] +) -> Optional[str]: nvidia_sub_dirs = ("nvidia", "*", "nvvm", "lib64") if libname == "nvvm" else ("nvidia", "*", "lib") file_wild = so_basename + "*" for lib_dir in find_sub_dirs_all_sitepackages(nvidia_sub_dirs): @@ -34,7 +44,7 @@ def _find_so_using_nvidia_lib_dirs(libname, so_basename, error_messages, attachm return None -def _find_dll_under_dir(dirpath, file_wild): +def _find_dll_under_dir(dirpath: str, file_wild: str) -> Optional[str]: for path in sorted(glob.glob(os.path.join(dirpath, file_wild))): if not os.path.isfile(path): continue @@ -43,7 +53,9 @@ def _find_dll_under_dir(dirpath, file_wild): return None -def _find_dll_using_nvidia_bin_dirs(libname, lib_searched_for, error_messages, attachments): +def _find_dll_using_nvidia_bin_dirs( + libname: str, lib_searched_for: str, error_messages: list[str], attachments: list[str] +) -> Optional[str]: nvidia_sub_dirs = ("nvidia", "*", "nvvm", "bin") if libname == "nvvm" else ("nvidia", "*", "bin") for bin_dir in find_sub_dirs_all_sitepackages(nvidia_sub_dirs): dll_name = _find_dll_under_dir(bin_dir, lib_searched_for) @@ -53,17 +65,18 @@ def _find_dll_using_nvidia_bin_dirs(libname, lib_searched_for, error_messages, a return None -def _get_cuda_home(): +def _get_cuda_home() -> Optional[str]: cuda_home = os.environ.get("CUDA_HOME") if cuda_home is None: cuda_home = os.environ.get("CUDA_PATH") return cuda_home -def _find_lib_dir_using_cuda_home(libname): +def _find_lib_dir_using_cuda_home(libname: str) -> Optional[str]: cuda_home = _get_cuda_home() if cuda_home is None: return None + subdirs: tuple[str, ...] if IS_WINDOWS: subdirs = (os.path.join("nvvm", "bin"),) if libname == "nvvm" else ("bin",) else: @@ -82,7 +95,9 @@ def _find_lib_dir_using_cuda_home(libname): return None -def _find_so_using_lib_dir(lib_dir, so_basename, error_messages, attachments): +def _find_so_using_lib_dir( + lib_dir: str, so_basename: str, error_messages: list[str], attachments: list[str] +) -> Optional[str]: so_name = os.path.join(lib_dir, so_basename) if os.path.isfile(so_name): return so_name @@ -96,7 +111,9 @@ def _find_so_using_lib_dir(lib_dir, so_basename, error_messages, attachments): return None -def _find_dll_using_lib_dir(lib_dir, libname, error_messages, attachments): +def _find_dll_using_lib_dir( + lib_dir: str, libname: str, error_messages: list[str], attachments: list[str] +) -> Optional[str]: file_wild = libname + "*.dll" dll_name = _find_dll_under_dir(lib_dir, file_wild) if dll_name is not None: @@ -108,46 +125,58 @@ def _find_dll_using_lib_dir(lib_dir, libname, error_messages, attachments): return None -class _find_nvidia_dynamic_library: +class _find_nvidia_dynamic_lib: def __init__(self, libname: str): self.libname = libname - self.error_messages = [] - self.attachments = [] + self.error_messages: list[str] = [] + self.attachments: list[str] = [] self.abs_path = None if IS_WINDOWS: self.lib_searched_for = f"{libname}*.dll" if self.abs_path is None: self.abs_path = _find_dll_using_nvidia_bin_dirs( - libname, self.lib_searched_for, self.error_messages, self.attachments + libname, + self.lib_searched_for, + self.error_messages, + self.attachments, ) else: self.lib_searched_for = f"lib{libname}.so" if self.abs_path is None: self.abs_path = _find_so_using_nvidia_lib_dirs( - libname, self.lib_searched_for, self.error_messages, self.attachments + libname, + self.lib_searched_for, + self.error_messages, + self.attachments, ) - def retry_with_cuda_home_priority_last(self): + def retry_with_cuda_home_priority_last(self) -> None: cuda_home_lib_dir = _find_lib_dir_using_cuda_home(self.libname) if cuda_home_lib_dir is not None: if IS_WINDOWS: self.abs_path = _find_dll_using_lib_dir( - cuda_home_lib_dir, self.libname, self.error_messages, self.attachments + cuda_home_lib_dir, + self.libname, + self.error_messages, + self.attachments, ) else: self.abs_path = _find_so_using_lib_dir( - cuda_home_lib_dir, self.lib_searched_for, self.error_messages, self.attachments + cuda_home_lib_dir, + self.lib_searched_for, + self.error_messages, + self.attachments, ) - def raise_if_abs_path_is_None(self): + def raise_if_abs_path_is_None(self) -> str: if self.abs_path: return self.abs_path err = ", ".join(self.error_messages) att = "\n".join(self.attachments) - raise RuntimeError(f'Failure finding "{self.lib_searched_for}": {err}\n{att}') + raise DynamicLibNotFound(f'Failure finding "{self.lib_searched_for}": {err}\n{att}') @functools.cache -def find_nvidia_dynamic_library(libname: str) -> str: - return _find_nvidia_dynamic_library(libname).raise_if_abs_path_is_None() +def find_nvidia_dynamic_lib(libname: str) -> str: + return _find_nvidia_dynamic_lib(libname).raise_if_abs_path_is_None() diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_common.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_common.py new file mode 100644 index 000000000..1b6fef9e8 --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_common.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from dataclasses import dataclass +from typing import Callable, Optional + +from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import ( + DIRECT_DEPENDENCIES, + IS_WINDOWS, +) + +if IS_WINDOWS: + import pywintypes + + HandleType = pywintypes.HANDLE +else: + HandleType = int + + +class DynamicLibNotFound(RuntimeError): + pass + + +@dataclass +class LoadedDL: + handle: HandleType # type: ignore[valid-type] + abs_path: Optional[str] + was_already_loaded_from_elsewhere: bool + + +def load_dependencies(libname: str, load_func: Callable[[str], LoadedDL]) -> None: + for dep in DIRECT_DEPENDENCIES.get(libname, ()): + load_func(dep) diff --git a/cuda_bindings/cuda/bindings/_path_finder/load_dl_linux.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py similarity index 88% rename from cuda_bindings/cuda/bindings/_path_finder/load_dl_linux.py rename to cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py index b9f3839e1..ac080ec6e 100644 --- a/cuda_bindings/cuda/bindings/_path_finder/load_dl_linux.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_linux.py @@ -1,12 +1,12 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 import ctypes import ctypes.util import os from typing import Optional -from cuda.bindings._path_finder.load_dl_common import LoadedDL +from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL CDLL_MODE = os.RTLD_NOW | os.RTLD_GLOBAL @@ -40,7 +40,7 @@ def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> Optional[ Raises: OSError: If dladdr fails to get information about the symbol """ - from cuda.bindings._path_finder.supported_libs import EXPECTED_LIB_SYMBOLS + from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import EXPECTED_LIB_SYMBOLS for symbol_name in EXPECTED_LIB_SYMBOLS[libname]: symbol = getattr(handle, symbol_name, None) @@ -53,7 +53,7 @@ def abs_path_for_dynamic_library(libname: str, handle: ctypes.CDLL) -> Optional[ info = Dl_info() if LIBDL.dladdr(addr, ctypes.byref(info)) == 0: raise OSError(f"dladdr failed for {libname=!r}") - return info.dli_fname.decode() + return info.dli_fname.decode() # type: ignore[no-any-return] def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]: @@ -70,7 +70,7 @@ def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]: >>> if loaded is not None: ... print(f"Library already loaded from {loaded.abs_path}") """ - from cuda.bindings._path_finder.supported_libs import SUPPORTED_LINUX_SONAMES + from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_LINUX_SONAMES for soname in SUPPORTED_LINUX_SONAMES.get(libname, ()): try: diff --git a/cuda_bindings/cuda/bindings/_path_finder/load_dl_windows.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_windows.py similarity index 80% rename from cuda_bindings/cuda/bindings/_path_finder/load_dl_windows.py rename to cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_windows.py index 0d13680a6..3bf9653d2 100644 --- a/cuda_bindings/cuda/bindings/_path_finder/load_dl_windows.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_windows.py @@ -1,12 +1,12 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 from typing import Optional import pywintypes import win32api -from cuda.bindings._path_finder.load_dl_common import LoadedDL +from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL # Mirrors WinBase.h (unfortunately not defined already elsewhere) WINBASE_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100 @@ -27,7 +27,7 @@ def add_dll_directory(dll_abs_path: str) -> None: dirpath = os.path.dirname(dll_abs_path) assert os.path.isdir(dirpath), dll_abs_path # Add the DLL directory to the search path - os.add_dll_directory(dirpath) + os.add_dll_directory(dirpath) # type: ignore[attr-defined] # Update PATH as a fallback for dependent DLL resolution curr_path = os.environ.get("PATH") os.environ["PATH"] = dirpath if curr_path is None else os.pathsep.join((curr_path, dirpath)) @@ -36,7 +36,7 @@ def add_dll_directory(dll_abs_path: str) -> None: def abs_path_for_dynamic_library(libname: str, handle: pywintypes.HANDLE) -> str: """Get the absolute path of a loaded dynamic library on Windows.""" try: - return win32api.GetModuleFileName(handle) + return win32api.GetModuleFileName(handle) # type: ignore[no-any-return] except Exception as e: raise RuntimeError(f"GetModuleFileName failed for {libname!r} (exception type: {type(e)})") from e @@ -55,7 +55,7 @@ def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]: >>> if loaded is not None: ... print(f"Library already loaded from {loaded.abs_path}") """ - from cuda.bindings._path_finder.supported_libs import SUPPORTED_WINDOWS_DLLS + from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_WINDOWS_DLLS for dll_name in SUPPORTED_WINDOWS_DLLS.get(libname, ()): try: @@ -67,17 +67,17 @@ def check_if_already_loaded_from_elsewhere(libname: str) -> Optional[LoadedDL]: return None -def load_with_system_search(libname: str, _unused: str) -> Optional[LoadedDL]: +def load_with_system_search(libname: str, soname: str) -> Optional[LoadedDL]: """Try to load a DLL using system search paths. Args: libname: The name of the library to load - _unused: Unused parameter (kept for interface consistency) + soname: Unused parameter (kept for interface consistency) Returns: A LoadedDL object if successful, None if the library cannot be loaded """ - from cuda.bindings._path_finder.supported_libs import SUPPORTED_WINDOWS_DLLS + from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import SUPPORTED_WINDOWS_DLLS for dll_name in SUPPORTED_WINDOWS_DLLS.get(libname, ()): try: @@ -103,7 +103,9 @@ def load_with_abs_path(libname: str, found_path: str) -> LoadedDL: Raises: RuntimeError: If the DLL cannot be loaded """ - from cuda.bindings._path_finder.supported_libs import LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY + from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import ( + LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY, + ) if libname in LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY: add_dll_directory(found_path) diff --git a/cuda_bindings/cuda/bindings/_path_finder/load_nvidia_dynamic_library.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py similarity index 53% rename from cuda_bindings/cuda/bindings/_path_finder/load_nvidia_dynamic_library.py rename to cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py index f8fe5ce4a..101088e60 100644 --- a/cuda_bindings/cuda/bindings/_path_finder/load_nvidia_dynamic_library.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py @@ -1,27 +1,29 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 import functools +import struct +import sys -from cuda.bindings._path_finder.find_nvidia_dynamic_library import _find_nvidia_dynamic_library -from cuda.bindings._path_finder.load_dl_common import LoadedDL, load_dependencies -from cuda.bindings._path_finder.supported_libs import IS_WINDOWS +from cuda.pathfinder._dynamic_libs.find_nvidia_dynamic_lib import _find_nvidia_dynamic_lib +from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL, load_dependencies +from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import IS_WINDOWS if IS_WINDOWS: - from cuda.bindings._path_finder.load_dl_windows import ( + from cuda.pathfinder._dynamic_libs.load_dl_windows import ( check_if_already_loaded_from_elsewhere, load_with_abs_path, load_with_system_search, ) else: - from cuda.bindings._path_finder.load_dl_linux import ( + from cuda.pathfinder._dynamic_libs.load_dl_linux import ( check_if_already_loaded_from_elsewhere, load_with_abs_path, load_with_system_search, ) -def _load_nvidia_dynamic_library_no_cache(libname: str) -> LoadedDL: +def _load_lib_no_cache(libname: str) -> LoadedDL: # Check whether the library is already loaded into the current process by # some other component. This check uses OS-level mechanisms (e.g., # dlopen on Linux, GetModuleHandle on Windows). @@ -30,10 +32,10 @@ def _load_nvidia_dynamic_library_no_cache(libname: str) -> LoadedDL: return loaded # Load dependencies first - load_dependencies(libname, load_nvidia_dynamic_library) + load_dependencies(libname, load_nvidia_dynamic_lib) # Find the library path - found = _find_nvidia_dynamic_library(libname) + found = _find_nvidia_dynamic_lib(libname) if found.abs_path is None: loaded = load_with_system_search(libname, found.lib_searched_for) if loaded is not None: @@ -42,11 +44,12 @@ def _load_nvidia_dynamic_library_no_cache(libname: str) -> LoadedDL: found.raise_if_abs_path_is_None() # Load the library from the found path + assert found.abs_path is not None # for mypy return load_with_abs_path(libname, found.abs_path) @functools.cache -def load_nvidia_dynamic_library(libname: str) -> LoadedDL: +def load_nvidia_dynamic_lib(libname: str) -> LoadedDL: """Load a NVIDIA dynamic library by name. Args: @@ -58,4 +61,11 @@ def load_nvidia_dynamic_library(libname: str) -> LoadedDL: Raises: RuntimeError: If the library cannot be found or loaded """ - return _load_nvidia_dynamic_library_no_cache(libname) + pointer_size_bits = struct.calcsize("P") * 8 + if pointer_size_bits != 64: + raise RuntimeError( + f"cuda.pathfinder.load_nvidia_dynamic_lib() requires 64-bit Python." + f" Currently running: {pointer_size_bits}-bit Python" + f" {sys.version_info.major}.{sys.version_info.minor}" + ) + return _load_lib_no_cache(libname) diff --git a/cuda_bindings/cuda/bindings/_path_finder/supported_libs.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/supported_nvidia_libs.py similarity index 88% rename from cuda_bindings/cuda/bindings/_path_finder/supported_libs.py rename to cuda_pathfinder/cuda/pathfinder/_dynamic_libs/supported_nvidia_libs.py index 63bde282f..19d73b23e 100644 --- a/cuda_bindings/cuda/bindings/_path_finder/supported_libs.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/supported_nvidia_libs.py @@ -1,23 +1,24 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 # THIS FILE NEEDS TO BE REVIEWED/UPDATED FOR EACH CTK RELEASE +# Likely candidates for updates are: +# SUPPORTED_LIBNAMES +# SUPPORTED_WINDOWS_DLLS +# SUPPORTED_LINUX_SONAMES +# EXPECTED_LIB_SYMBOLS import sys IS_WINDOWS = sys.platform == "win32" -SUPPORTED_LIBNAMES = ( +SUPPORTED_LIBNAMES_COMMON = ( # Core CUDA Runtime and Compiler + "cudart", + "nvfatbin", "nvJitLink", "nvrtc", "nvvm", -) - -PARTIALLY_SUPPORTED_LIBNAMES_COMMON = ( - # Core CUDA Runtime and Compiler - "cudart", - "nvfatbin", # Math Libraries "cublas", "cublasLt", @@ -50,27 +51,17 @@ # and limited availability. Keeping this as a reference avoids having to # reconstruct the information from scratch in the future. -PARTIALLY_SUPPORTED_LIBNAMES_LINUX_ONLY = ( +SUPPORTED_LIBNAMES_LINUX_ONLY = ( "cufile", # "cufile_rdma", # Requires libmlx5.so ) +SUPPORTED_LIBNAMES_LINUX = SUPPORTED_LIBNAMES_COMMON + SUPPORTED_LIBNAMES_LINUX_ONLY -PARTIALLY_SUPPORTED_LIBNAMES_LINUX = PARTIALLY_SUPPORTED_LIBNAMES_COMMON + PARTIALLY_SUPPORTED_LIBNAMES_LINUX_ONLY - -PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS_ONLY = () - -PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS = PARTIALLY_SUPPORTED_LIBNAMES_COMMON + PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS_ONLY - -PARTIALLY_SUPPORTED_LIBNAMES_ALL = ( - PARTIALLY_SUPPORTED_LIBNAMES_COMMON - + PARTIALLY_SUPPORTED_LIBNAMES_LINUX_ONLY - + PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS_ONLY -) +SUPPORTED_LIBNAMES_WINDOWS_ONLY = () +SUPPORTED_LIBNAMES_WINDOWS = SUPPORTED_LIBNAMES_COMMON + SUPPORTED_LIBNAMES_WINDOWS_ONLY -if IS_WINDOWS: - PARTIALLY_SUPPORTED_LIBNAMES = PARTIALLY_SUPPORTED_LIBNAMES_WINDOWS -else: - PARTIALLY_SUPPORTED_LIBNAMES = PARTIALLY_SUPPORTED_LIBNAMES_LINUX +SUPPORTED_LIBNAMES_ALL = SUPPORTED_LIBNAMES_COMMON + SUPPORTED_LIBNAMES_LINUX_ONLY + SUPPORTED_LIBNAMES_WINDOWS_ONLY +SUPPORTED_LIBNAMES = SUPPORTED_LIBNAMES_WINDOWS if IS_WINDOWS else SUPPORTED_LIBNAMES_LINUX # Based on ldd output for Linux x86_64 nvidia-*-cu12 wheels (12.8.1) DIRECT_DEPENDENCIES = { @@ -112,7 +103,7 @@ # cuda_12.6.2_560.35.03_linux.run # cuda_12.8.0_570.86.10_linux.run # cuda_12.9.0_575.51.03_linux.run -# Generated with toolshed/build_path_finder_sonames.py +# Generated with toolshed/build_pathfinder_sonames.py SUPPORTED_LINUX_SONAMES = { "cublas": ( "libcublas.so.11", @@ -234,7 +225,7 @@ # cuda_12.6.2_560.94_windows.exe # cuda_12.8.1_572.61_windows.exe # cuda_12.9.0_576.02_windows.txt -# Generated with toolshed/build_path_finder_dlls.py (WITH MANUAL EDITS) +# Generated with toolshed/build_pathfinder_dlls.py (WITH MANUAL EDITS) SUPPORTED_WINDOWS_DLLS = { "cublas": ( "cublas64_11.dll", @@ -245,9 +236,6 @@ "cublasLt64_12.dll", ), "cudart": ( - "cudart32_110.dll", - "cudart32_65.dll", - "cudart32_90.dll", "cudart64_101.dll", "cudart64_110.dll", "cudart64_12.dll", @@ -335,7 +323,6 @@ "nvrtc64_120_0.dll", ), "nvvm": ( - "nvvm32.dll", "nvvm64.dll", "nvvm64_33_0.dll", "nvvm64_40_0.dll", @@ -357,7 +344,7 @@ def is_suppressed_dll_file(path_basename: str) -> bool: # nvrtc64_120_0.alt.dll # nvrtc64_120_0.dll return path_basename.endswith(".alt.dll") or "-builtins" in path_basename - return False + return path_basename.startswith(("cudart32_", "nvvm32")) # Based on nm output for Linux x86_64 /usr/local/cuda (12.8.1) diff --git a/cuda_bindings/cuda/bindings/_path_finder/find_sub_dirs.py b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py similarity index 68% rename from cuda_bindings/cuda/bindings/_path_finder/find_sub_dirs.py rename to cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py index 810132625..b05467e52 100644 --- a/cuda_bindings/cuda/bindings/_path_finder/find_sub_dirs.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py @@ -1,13 +1,14 @@ -# Copyright 2024-2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 import functools import os import site import sys +from collections.abc import Sequence -def find_sub_dirs_no_cache(parent_dirs, sub_dirs): +def find_sub_dirs_no_cache(parent_dirs: Sequence[str], sub_dirs: Sequence[str]) -> list[str]: results = [] for base in parent_dirs: stack = [(base, 0)] # (current_path, index into sub_dirs) @@ -36,17 +37,17 @@ def find_sub_dirs_no_cache(parent_dirs, sub_dirs): @functools.cache -def find_sub_dirs_cached(parent_dirs, sub_dirs): +def find_sub_dirs_cached(parent_dirs: Sequence[str], sub_dirs: Sequence[str]) -> list[str]: return find_sub_dirs_no_cache(parent_dirs, sub_dirs) -def find_sub_dirs(parent_dirs, sub_dirs): +def find_sub_dirs(parent_dirs: Sequence[str], sub_dirs: Sequence[str]) -> list[str]: return find_sub_dirs_cached(tuple(parent_dirs), tuple(sub_dirs)) -def find_sub_dirs_sys_path(sub_dirs): +def find_sub_dirs_sys_path(sub_dirs: Sequence[str]) -> list[str]: return find_sub_dirs(sys.path, sub_dirs) -def find_sub_dirs_all_sitepackages(sub_dirs): +def find_sub_dirs_all_sitepackages(sub_dirs: Sequence[str]) -> list[str]: return find_sub_dirs((site.getusersitepackages(),) + tuple(site.getsitepackages()), sub_dirs) diff --git a/cuda_pathfinder/cuda/pathfinder/_version.py b/cuda_pathfinder/cuda/pathfinder/_version.py new file mode 100644 index 000000000..23e58fddb --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_version.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +__version__ = "1.0.0" diff --git a/cuda_pathfinder/mypy.ini b/cuda_pathfinder/mypy.ini new file mode 100644 index 000000000..8eb52bb92 --- /dev/null +++ b/cuda_pathfinder/mypy.ini @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[mypy] +# Basic settings +python_version = 3.9 +explicit_package_bases = True +warn_return_any = True +warn_unused_configs = True +disallow_untyped_defs = True + +# Start strict, but allow some flexibility +check_untyped_defs = True +disallow_any_generics = True +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = True + +# Allow some common patterns to keep it simple +allow_redefinition = True +implicit_reexport = True + +# Ignore missing imports for now (you can tighten this later) +ignore_missing_imports = True + +# Only check your package +[mypy-cuda.pathfinder.*] +disallow_untyped_defs = True + +# Be more lenient with test files +[mypy-tests.*] +disallow_untyped_defs = False +ignore_errors = True diff --git a/cuda_pathfinder/pyproject.toml b/cuda_pathfinder/pyproject.toml new file mode 100644 index 000000000..5c705cad4 --- /dev/null +++ b/cuda_pathfinder/pyproject.toml @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[project] +name = "cuda-pathfinder" +description = "Pathfinder for CUDA components" +authors = [{ name = "NVIDIA Corporation", email = "cuda-python-conduct@nvidia.com" }] +license = "Apache-2.0" +requires-python = ">=3.9" +dynamic = ["version"] +dependencies = [ + "pywin32; sys_platform == 'win32'", +] + +[project.optional-dependencies] +test = [ + "pytest>=6.2.4", +] +nvidia_wheels_cu12 = [ + "nvidia-cublas-cu12", + "nvidia-cuda-nvcc-cu12", + "nvidia-cuda-nvrtc-cu12", + "nvidia-cuda-runtime-cu12", + "nvidia-cufft-cu12", + "nvidia-cufile-cu12; sys_platform != 'win32'", + "nvidia-curand-cu12", + "nvidia-cusolver-cu12", + "nvidia-cusparse-cu12", + "nvidia-npp-cu12", + "nvidia-nvfatbin-cu12", + "nvidia-nvjitlink-cu12", + "nvidia-nvjpeg-cu12", +] + +[project.urls] +Repository = "https://github.com/NVIDIA/cuda-python" +Documentation = "https://nvidia.github.io/cuda-python/" + +[tool.setuptools] +packages = { find = { include = ["cuda*"] } } + +[tool.setuptools.dynamic] +version = { attr = "cuda.pathfinder._version.__version__" } +readme = { file = ["DESCRIPTION.rst"], content-type = "text/x-rst" } + +[build-system] +requires = ["setuptools>=64", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.ruff] +line-length = 120 +preview = true + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = [ + # pycodestyle Error + "E", + # Pyflakes + "F", + # pycodestyle Warning + "W", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", +] diff --git a/cuda_bindings/tests/conftest.py b/cuda_pathfinder/tests/conftest.py similarity index 78% rename from cuda_bindings/tests/conftest.py rename to cuda_pathfinder/tests/conftest.py index aa31f1376..1069de59b 100644 --- a/cuda_bindings/tests/conftest.py +++ b/cuda_pathfinder/tests/conftest.py @@ -1,5 +1,5 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 import pytest diff --git a/cuda_bindings/tests/spawned_process_runner.py b/cuda_pathfinder/tests/spawned_process_runner.py similarity index 94% rename from cuda_bindings/tests/spawned_process_runner.py rename to cuda_pathfinder/tests/spawned_process_runner.py index 3a13362fe..dea460a26 100644 --- a/cuda_bindings/tests/spawned_process_runner.py +++ b/cuda_pathfinder/tests/spawned_process_runner.py @@ -1,13 +1,14 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 import multiprocessing import queue # for Empty import sys import traceback +from collections.abc import Sequence from dataclasses import dataclass from io import StringIO -from typing import Any, Callable, Optional, Sequence +from typing import Any, Callable, Optional PROCESS_KILLED = -9 PROCESS_NO_RESULT = -999 diff --git a/cuda_pathfinder/tests/test_load_nvidia_dynamic_lib.py b/cuda_pathfinder/tests/test_load_nvidia_dynamic_lib.py new file mode 100644 index 000000000..ee6b322b2 --- /dev/null +++ b/cuda_pathfinder/tests/test_load_nvidia_dynamic_lib.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os +from unittest.mock import patch + +import pytest +import spawned_process_runner + +from cuda.pathfinder import SUPPORTED_NVIDIA_LIBNAMES, load_nvidia_dynamic_lib +from cuda.pathfinder._dynamic_libs import supported_nvidia_libs +from cuda.pathfinder._dynamic_libs.load_dl_common import IS_WINDOWS + +STRICTNESS = os.environ.get("CUDA_PATHFINDER_TEST_LOAD_NVIDIA_DYNAMIC_LIB_STRICTNESS", "see_what_works") +assert STRICTNESS in ("see_what_works", "all_must_work") + + +def test_supported_libnames_linux_sonames_consistency(): + assert tuple(sorted(supported_nvidia_libs.SUPPORTED_LIBNAMES_LINUX)) == tuple( + sorted(supported_nvidia_libs.SUPPORTED_LINUX_SONAMES.keys()) + ) + + +def test_supported_libnames_windows_dlls_consistency(): + assert tuple(sorted(supported_nvidia_libs.SUPPORTED_LIBNAMES_WINDOWS)) == tuple( + sorted(supported_nvidia_libs.SUPPORTED_WINDOWS_DLLS.keys()) + ) + + +@pytest.mark.parametrize("dict_name", ["SUPPORTED_LINUX_SONAMES", "SUPPORTED_WINDOWS_DLLS"]) +def test_libname_dict_values_are_unique(dict_name): + libname_dict = getattr(supported_nvidia_libs, dict_name) + libname_for_value = {} + for libname, values in libname_dict.items(): + for value in values: + prev_libname = libname_for_value.get(value) + if prev_libname is not None: + raise RuntimeError(f"Multiple libnames for {value!r}: {prev_libname}, {libname}") + libname_for_value[value] = libname + + +def test_supported_libnames_windows_libnames_requiring_os_add_dll_directory_consistency(): + assert not ( + set(supported_nvidia_libs.LIBNAMES_REQUIRING_OS_ADD_DLL_DIRECTORY) + - set(supported_nvidia_libs.SUPPORTED_LIBNAMES_WINDOWS) + ) + + +def test_supported_libnames_all_expected_lib_symbols_consistency(): + assert tuple(sorted(supported_nvidia_libs.SUPPORTED_LIBNAMES_ALL)) == tuple( + sorted(supported_nvidia_libs.EXPECTED_LIB_SYMBOLS.keys()) + ) + + +def test_runtime_error_on_non_64bit_python(): + with ( + patch("struct.calcsize", return_value=3), # fake 24-bit pointer + pytest.raises(RuntimeError, match=r"requires 64-bit Python\. Currently running: 24-bit Python"), + ): + load_nvidia_dynamic_lib("not_used") + + +def build_child_process_failed_for_libname_message(libname, result): + return ( + f"Child process failed for {libname=!r} with exit code {result.returncode}\n" + f"--- stdout-from-child-process ---\n{result.stdout}\n" + f"--- stderr-from-child-process ---\n{result.stderr}\n" + ) + + +def child_process_func(libname): + import os + + from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import _load_lib_no_cache + + loaded_dl_fresh = load_nvidia_dynamic_lib(libname) + if loaded_dl_fresh.was_already_loaded_from_elsewhere: + raise RuntimeError("loaded_dl_fresh.was_already_loaded_from_elsewhere") + + loaded_dl_from_cache = load_nvidia_dynamic_lib(libname) + if loaded_dl_from_cache is not loaded_dl_fresh: + raise RuntimeError("loaded_dl_from_cache is not loaded_dl_fresh") + + loaded_dl_no_cache = _load_lib_no_cache(libname) + if not loaded_dl_no_cache.was_already_loaded_from_elsewhere: + raise RuntimeError("loaded_dl_no_cache.was_already_loaded_from_elsewhere") + if not os.path.samefile(loaded_dl_no_cache.abs_path, loaded_dl_fresh.abs_path): + raise RuntimeError(f"not os.path.samefile({loaded_dl_no_cache.abs_path=!r}, {loaded_dl_fresh.abs_path=!r})") + + print(f"{loaded_dl_fresh.abs_path!r}") + + +@pytest.mark.parametrize("libname", SUPPORTED_NVIDIA_LIBNAMES) +def test_load_nvidia_dynamic_lib(info_summary_append, libname): + # We intentionally run each dynamic library operation in a child process + # to ensure isolation of global dynamic linking state (e.g., dlopen handles). + # Without child processes, loading/unloading libraries during testing could + # interfere across test cases and lead to nondeterministic or platform-specific failures. + timeout = 120 if IS_WINDOWS else 30 + result = spawned_process_runner.run_in_spawned_child_process(child_process_func, args=(libname,), timeout=timeout) + if result.returncode == 0: + info_summary_append(f"abs_path={result.stdout.rstrip()}") + elif STRICTNESS == "see_what_works" or "DynamicLibNotFound: Failure finding " in result.stderr: + info_summary_append(f"Not found: {libname=!r}") + else: + raise RuntimeError(build_child_process_failed_for_libname_message(libname, result)) diff --git a/cuda_bindings/tests/test_spawned_process_runner.py b/cuda_pathfinder/tests/test_spawned_process_runner.py similarity index 68% rename from cuda_bindings/tests/test_spawned_process_runner.py rename to cuda_pathfinder/tests/test_spawned_process_runner.py index 644ed8a83..98303adb7 100644 --- a/cuda_bindings/tests/test_spawned_process_runner.py +++ b/cuda_pathfinder/tests/test_spawned_process_runner.py @@ -1,7 +1,7 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 -# Note: This only covers what is not covered already in test_path_finder_load.py +# Note: This only covers what is not covered already in test_nvidia_dynamic_libs_load_lib.py import pytest from spawned_process_runner import run_in_spawned_child_process diff --git a/cuda_bindings/tests/test_path_finder_find_sub_dirs.py b/cuda_pathfinder/tests/test_utils_find_sub_dirs.py similarity index 92% rename from cuda_bindings/tests/test_path_finder_find_sub_dirs.py rename to cuda_pathfinder/tests/test_utils_find_sub_dirs.py index 6b2644bff..80bab2529 100644 --- a/cuda_bindings/tests/test_path_finder_find_sub_dirs.py +++ b/cuda_pathfinder/tests/test_utils_find_sub_dirs.py @@ -1,11 +1,11 @@ -# Copyright 2025 NVIDIA Corporation. All rights reserved. -# SPDX-License-Identifier: LicenseRef-NVIDIA-SOFTWARE-LICENSE +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 import os import pytest -from cuda.bindings._path_finder.find_sub_dirs import ( +from cuda.pathfinder._utils.find_sub_dirs import ( find_sub_dirs, find_sub_dirs_all_sitepackages, find_sub_dirs_sys_path, diff --git a/toolshed/build_path_finder_dlls.py b/toolshed/build_pathfinder_dlls.py similarity index 56% rename from toolshed/build_path_finder_dlls.py rename to toolshed/build_pathfinder_dlls.py index be2db0d1f..3c62a9386 100755 --- a/toolshed/build_path_finder_dlls.py +++ b/toolshed/build_pathfinder_dlls.py @@ -7,24 +7,26 @@ # Input for this script: .txt files generated with: # for exe in *.exe; do 7z l $exe > "${exe%.exe}.txt"; done -# The output of this script -# requires obvious manual edits to remove duplicates and unwanted dlls. +# The output of this script is expected to be usable as-is. +import collections import sys -LIBNAMES_IN_SCOPE_OF_CUDA_BINDINGS_PATH_FINDER = ( +# ATTENTION: Ambiguous shorter names need to appear after matching longer names +# (e.g. "cufft" after "cufftw") +LIBNAMES_IN_SCOPE_OF_CUDA_PATHFINDER = ( "nvJitLink", "nvrtc", "nvvm", "cudart", "nvfatbin", - "cublas", "cublasLt", - "cufft", + "cublas", "cufftw", + "cufft", "curand", - "cusolver", "cusolverMg", + "cusolver", "cusparse", "nppc", "nppial", @@ -38,12 +40,25 @@ "nppitc", "npps", "nvblas", - "cufile", - "cufile_rdma", "nvjpeg", ) +def is_suppressed_dll(libname, dll): + if libname == "cudart": + if dll.startswith("cudart32_"): + return True + elif libname == "nvrtc": + if dll.endswith(".alt.dll"): + return True + if dll.startswith("nvrtc-builtins"): + return True + elif libname == "nvvm": + if dll == "nvvm32.dll": + return True + return False + + def run(args): dlls_from_files = set() for filename in args: @@ -65,18 +80,32 @@ def run(args): else: raise RuntimeError("------------------- NOT FOUND") - print("DLLs in scope of cuda.bindings.path_finder") - print("==========================================") + print("DLLs in scope of cuda.pathfinder") + print("================================") dlls_in_scope = set() - for libname in sorted(LIBNAMES_IN_SCOPE_OF_CUDA_BINDINGS_PATH_FINDER): - print(f'"{libname}": (') + dlls_by_libname = collections.defaultdict(list) + suppressed_dlls = set() + for libname in LIBNAMES_IN_SCOPE_OF_CUDA_PATHFINDER: for dll in sorted(dlls_from_files): - if dll.startswith(libname): + if dll not in dlls_in_scope and dll.startswith(libname): + if is_suppressed_dll(libname, dll): + suppressed_dlls.add(dll) + else: + dlls_by_libname[libname].append(dll) dlls_in_scope.add(dll) - print(f' "{dll}",') + for libname, dlls in sorted(dlls_by_libname.items()): + print(f'"{libname}": (') + for dll in dlls: + print(f' "{dll}",') print("),") print() + print("Suppressed DLLs") + print("===============") + for dll in sorted(suppressed_dlls): + print(dll) + print() + print("DLLs out of scope") print("=================") for dll in sorted(dlls_from_files - dlls_in_scope): diff --git a/toolshed/build_path_finder_sonames.py b/toolshed/build_pathfinder_sonames.py similarity index 82% rename from toolshed/build_path_finder_sonames.py rename to toolshed/build_pathfinder_sonames.py index 17b7dd7b3..3cdaf185a 100755 --- a/toolshed/build_path_finder_sonames.py +++ b/toolshed/build_pathfinder_sonames.py @@ -7,12 +7,11 @@ # Input for this script: # output of toolshed/find_sonames.sh -# The output of this script -# is expected to be usable as-is. +# The output of this script is expected to be usable as-is. import sys -LIBNAMES_IN_SCOPE_OF_CUDA_BINDINGS_PATH_FINDER = ( +LIBNAMES_IN_SCOPE_OF_CUDA_PATHFINDER = ( "nvJitLink", "nvrtc", "nvvm", @@ -54,10 +53,10 @@ def run(args): if flds[-1] != "SONAME_NOT_SET": sonames_from_file.add(flds[-1]) - print("SONAMEs in scope of cuda.bindings.path_finder") - print("=============================================") + print("SONAMEs in scope of cuda.pathfinder") + print("===================================") sonames_in_scope = set() - for libname in sorted(LIBNAMES_IN_SCOPE_OF_CUDA_BINDINGS_PATH_FINDER): + for libname in sorted(LIBNAMES_IN_SCOPE_OF_CUDA_PATHFINDER): print(f'"{libname}": (') lib_so = "lib" + libname + ".so" for soname in sorted(sonames_from_file): diff --git a/toolshed/run_cuda_bindings_path_finder.py b/toolshed/run_cuda_pathfinder.py similarity index 67% rename from toolshed/run_cuda_bindings_path_finder.py rename to toolshed/run_cuda_pathfinder.py index ca2193a81..cf197c837 100644 --- a/toolshed/run_cuda_bindings_path_finder.py +++ b/toolshed/run_cuda_pathfinder.py @@ -5,24 +5,19 @@ import sys import traceback -from cuda.bindings import path_finder -from cuda.bindings._path_finder import supported_libs - -ALL_LIBNAMES = ( - path_finder._SUPPORTED_LIBNAMES + supported_libs.PARTIALLY_SUPPORTED_LIBNAMES -) +from cuda import pathfinder def run(args): if args: libnames = args else: - libnames = ALL_LIBNAMES + libnames = pathfinder.SUPPORTED_NVIDIA_LIBNAMES for libname in libnames: print(f"{libname=}") try: - loaded_dl = path_finder._load_nvidia_dynamic_library(libname) + loaded_dl = pathfinder.load_nvidia_dynamic_lib(libname) except Exception: print(f"EXCEPTION for {libname=}:") traceback.print_exc(file=sys.stdout)