From 077f30b51a088adc28e246ad390ff2d64d502466 Mon Sep 17 00:00:00 2001 From: "behnam.asadi" Date: Wed, 29 Jul 2026 10:54:39 +0200 Subject: [PATCH 1/5] [fix] Let wheels resolve CUDA math libraries from pip packages libcuvslam.so links against cuBLAS, cuSOLVER and cuSPARSE, which are excluded from the wheel during auditwheel repair and were neither bundled nor declared anywhere. On a machine without a CUDA Toolkit installation, `pip install cuvslam-*.whl` succeeds and `import cuvslam` then fails with: ImportError: libcusolver.so.11: cannot open shared object file Declare the missing libraries as cu12/cu13 extras so pip can provide them, and load them from the nvidia-* pip layout before the extension module is imported: pip installs them under /nvidia/*/lib, which the dynamic loader does not search, so declaring them alone is not enough. When they cannot be resolved at all, the ImportError now names the two ways to provide them instead of just the missing soname. The wheel verification script could not catch this because it runs in a CUDA devel image that provides those libraries system-wide. It now also installs the wheel with its CUDA extra into a clean environment and requires that the loaded libraries are the pip-provided ones. Fixes #70 Signed-off-by: behnam.asadi --- README.md | 10 +++ python/CMakeLists.txt | 1 + python/__init__.py | 16 +++- python/_cuda_libs.py | 86 +++++++++++++++++++++ python/pyproject.toml | 19 +++++ python/test/test_cuda_libs.py | 84 ++++++++++++++++++++ scripts/verify_pycuvslam_wheel_in_docker.sh | 50 ++++++++++++ 7 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 python/_cuda_libs.py create mode 100644 python/test/test_cuda_libs.py diff --git a/README.md b/README.md index e746380..56e5366 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,14 @@ and are compatible with Python 3.12 and later. Other Python, CUDA, or Jetson com **Prerequisite**: [CUDA Toolkit 12 or 13](https://developer.nvidia.com/cuda/toolkit) must be installed separately (not included in the wheels). Its major version must match the wheel's `cu12` or `cu13` tag. +The wheels link against the CUDA math libraries (cuBLAS, cuSOLVER, cuSPARSE) but do not bundle them, so they must come +from that CUDA Toolkit installation. On systems without one, install the extra matching the wheel's tag instead and pip +provides them: + +```bash +pip install "cuvslam[cu12]" # or "cuvslam[cu13]" +``` + Official wheels include cuNLS support for `Multisensor` mode; no separate cuNLS installation is required. To install (virtual environment is recommended): @@ -135,6 +143,8 @@ To install (virtual environment is recommended): ```bash pip install cuvslam-*.whl +# ...or, without a CUDA Toolkit installation, with the CUDA math libraries from pip: +pip install "$(echo cuvslam-*.whl)[cu12]" ``` If a pre-built wheel is not available for your system, see [Install from Source](#install-from-source) below. diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index c77f436..71b4896 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -68,6 +68,7 @@ install(FILES ${CUVSLAM_LIBRARY} DESTINATION .) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py + ${CMAKE_CURRENT_SOURCE_DIR}/_cuda_libs.py ${CMAKE_CURRENT_SOURCE_DIR}/tracker.py ${CMAKE_CURRENT_SOURCE_DIR}/utils.py DESTINATION . diff --git a/python/__init__.py b/python/__init__.py index 9048259..70dd3ee 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -9,6 +9,20 @@ """cuVSLAM Python bindings.""" +from . import _cuda_libs + +# Make the CUDA math libraries this package links against, but does not bundle, loadable when they come from the +# nvidia-* pip packages instead of a CUDA Toolkit installation. No-op otherwise. +_cuda_libs.preload() + +try: + # Import all bindings under core namespace + from . import pycuvslam as core +except ImportError as error: + if 'cannot open shared object file' not in str(error): + raise + raise ImportError('{}\n\n{}'.format(error, _cuda_libs.MISSING_LIBRARY_HINT)) from error + # Import select bindings for the main namespace from .pycuvslam import ( get_version, @@ -25,8 +39,6 @@ PoseEstimate, Observation, Landmark) -# Import all bindings under core namespace -from . import pycuvslam as core # Import the wrapper class from .tracker import Tracker diff --git a/python/_cuda_libs.py b/python/_cuda_libs.py new file mode 100644 index 0000000..0849edf --- /dev/null +++ b/python/_cuda_libs.py @@ -0,0 +1,86 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# NVIDIA software released under the NVIDIA Community License is intended to be used to enable +# the further development of AI and robotics technologies. Such software has been designed, tested, +# and optimized for use with NVIDIA hardware, and this License grants permission to use the software +# solely with such hardware. +# Subject to the terms of this License, NVIDIA confirms that you are free to commercially use, +# modify, and distribute the software with NVIDIA hardware. NVIDIA does not claim ownership of any +# outputs generated using the software or derivative works thereof. Any code contributions that you +# share with NVIDIA are licensed to NVIDIA as feedback under this License and may be incorporated +# in future releases without notice or attribution. +# By using, reproducing, modifying, distributing, performing, or displaying any portion or element +# of the software or derivative works thereof, you agree to be bound by this License. + +"""Resolution of the CUDA math libraries that PyCuVSLAM links against but does not bundle. + +``libcuvslam.so`` links against cuBLAS, cuSOLVER, cuSPARSE and their dependencies. Wheels deliberately exclude them +during ``auditwheel repair`` (see ``scripts/build_pycuvslam_in_docker.sh``) because they are large, so they have to be +provided by the environment: either by a CUDA Toolkit installation, or by the ``nvidia-*`` pip packages that the +``cu12``/``cu13`` extras of this package declare. + +Pip installs those packages into ``/nvidia//lib``, a location the dynamic loader does not +search. Loading them here by absolute path, before the extension module is imported, registers their sonames with the +loader so ``libcuvslam.so`` resolves against them without the caller having to set ``LD_LIBRARY_PATH``. +""" + +import ctypes +import os +from glob import glob + +# Components of the pip CUDA layout that PyCuVSLAM needs, ordered so that a library is loaded after the libraries it +# depends on (cuSOLVER needs cuBLAS and cuSPARSE, which in turn need nvJitLink). preload() does not rely on the order +# being exactly right, it retries, but a good order keeps the common case to a single pass. +CUDA_COMPONENTS = ('nvjitlink', 'cublas', 'cusparse', 'cusolver') + +# Appended to the ImportError raised when the extension module cannot be loaded because of a missing library. +MISSING_LIBRARY_HINT = ( + "PyCuVSLAM links against the CUDA math libraries (cuBLAS, cuSOLVER, cuSPARSE) but does not bundle them.\n" + "Provide them either by installing a CUDA Toolkit whose major version matches this wheel's cu12/cu13 tag, or by\n" + "installing the matching pip packages:\n" + " pip install 'cuvslam[cu12]' # CUDA 12 wheels\n" + " pip install 'cuvslam[cu13]' # CUDA 13 wheels") + + +def nvidia_root(package_root=None): + """Return the directory the nvidia-* pip packages install into, next to this package.""" + if package_root is None: + package_root = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(os.path.dirname(package_root), 'nvidia') + + +def candidate_libraries(package_root=None): + """Return the pip-provided CUDA libraries to preload, in dependency order.""" + root = nvidia_root(package_root) + if not os.path.isdir(root): + return [] + candidates = [] + for component in CUDA_COMPONENTS: + candidates.extend(sorted(glob(os.path.join(root, component, 'lib', 'lib*.so.*')))) + return candidates + + +def preload(package_root=None): + """Load the pip-provided CUDA math libraries, if any, and return the paths that were loaded. + + Does nothing when the nvidia-* packages are not installed: the libraries are then expected to come from a system + CUDA Toolkit, which the loader finds on its own. + """ + pending = candidate_libraries(package_root) + loaded = [] + # A library whose own dependencies are not loaded yet fails to load, so sweep the list until a full pass makes no + # progress. Whatever is left unloaded is not reported here; the extension import below produces the actionable + # error message. + while pending: + remaining = [] + for path in pending: + try: + ctypes.CDLL(path, mode=ctypes.RTLD_GLOBAL) + except OSError: + remaining.append(path) + else: + loaded.append(path) + if len(remaining) == len(pending): + break + pending = remaining + return loaded diff --git a/python/pyproject.toml b/python/pyproject.toml index fc5effb..c59086a 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -24,6 +24,25 @@ classifiers = [ ] dependencies = ["pyyaml>=5.3.1"] +# libcuvslam.so links against the CUDA math libraries, but wheels exclude them during auditwheel repair +# (see scripts/build_pycuvslam_in_docker.sh) because of their size. They normally come from a CUDA Toolkit +# installation; these extras provide them from pip instead. Install the one matching the wheel's cu12/cu13 tag: +# `pip install "cuvslam[cu12]"`. CUDA 12 packages carry the -cu12 suffix; from CUDA 13 on, NVIDIA publishes the +# unsuffixed names and bumps them per CUDA release, so those are constrained to the matching major version. +[project.optional-dependencies] +cu12 = [ + "nvidia-cublas-cu12", + "nvidia-cusolver-cu12", + "nvidia-cusparse-cu12", + "nvidia-nvjitlink-cu12", +] +cu13 = [ + "nvidia-cublas>=13,<14", + "nvidia-cusolver>=12,<13", + "nvidia-cusparse>=12,<13", + "nvidia-nvjitlink>=13,<14", +] + [tool.scikit-build] minimum-version = "build-system.requires" cmake.version = "CMakeLists.txt" diff --git a/python/test/test_cuda_libs.py b/python/test/test_cuda_libs.py new file mode 100644 index 0000000..919253c --- /dev/null +++ b/python/test/test_cuda_libs.py @@ -0,0 +1,84 @@ +# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. +# +# NVIDIA software released under the NVIDIA Community License is intended to be used to enable +# the further development of AI and robotics technologies. Such software has been designed, tested, +# and optimized for use with NVIDIA hardware, and this License grants permission to use the software +# solely with such hardware. +# Subject to the terms of this License, NVIDIA confirms that you are free to commercially use, +# modify, and distribute the software with NVIDIA hardware. NVIDIA does not claim ownership of any +# outputs generated using the software or derivative works thereof. Any code contributions that you +# share with NVIDIA are licensed to NVIDIA as feedback under this License and may be incorporated +# in future releases without notice or attribution. +# By using, reproducing, modifying, distributing, performing, or displaying any portion or element +# of the software or derivative works thereof, you agree to be bound by this License. + +import os +import tempfile +import unittest + +from cuvslam import _cuda_libs + + +def make_pip_cuda_layout(root, components): + """Create a fake /nvidia layout next to a fake cuvslam package directory.""" + package_root = os.path.join(root, 'cuvslam') + os.makedirs(package_root) + for component, libraries in components.items(): + lib_dir = os.path.join(root, 'nvidia', component, 'lib') + os.makedirs(lib_dir) + for library in libraries: + # Not valid ELF: enough to be discovered, never loadable. + open(os.path.join(lib_dir, library), 'wb').close() + return package_root + + +class TestCudaLibs(unittest.TestCase): + def test_no_pip_cuda_packages_installed(self): + # The libraries then come from the system CUDA Toolkit and there is nothing to preload. + with tempfile.TemporaryDirectory() as root: + package_root = os.path.join(root, 'cuvslam') + os.makedirs(package_root) + self.assertEqual(_cuda_libs.candidate_libraries(package_root), []) + self.assertEqual(_cuda_libs.preload(package_root), []) + + def test_candidates_are_discovered_in_dependency_order(self): + with tempfile.TemporaryDirectory() as root: + package_root = make_pip_cuda_layout(root, { + 'cusolver': ['libcusolver.so.11'], + 'cublas': ['libcublas.so.12', 'libcublasLt.so.12'], + 'cusparse': ['libcusparse.so.12'], + 'nvjitlink': ['libnvJitLink.so.12']}) + found = [os.path.basename(path) for path in _cuda_libs.candidate_libraries(package_root)] + self.assertEqual(found, [ + 'libnvJitLink.so.12', 'libcublas.so.12', 'libcublasLt.so.12', 'libcusparse.so.12', + 'libcusolver.so.11']) + + def test_unrelated_components_are_ignored(self): + with tempfile.TemporaryDirectory() as root: + package_root = make_pip_cuda_layout(root, { + 'cudnn': ['libcudnn.so.9'], + 'cusparse': ['libcusparse.so.12']}) + found = [os.path.basename(path) for path in _cuda_libs.candidate_libraries(package_root)] + self.assertEqual(found, ['libcusparse.so.12']) + + def test_unloadable_libraries_do_not_raise(self): + # A library that cannot be loaded is left to the extension import to report, and the retry loop that + # tolerates an unknown load order must still terminate. + with tempfile.TemporaryDirectory() as root: + package_root = make_pip_cuda_layout(root, { + 'cublas': ['libcublas.so.12'], + 'cusolver': ['libcusolver.so.11']}) + self.assertEqual(_cuda_libs.preload(package_root), []) + + def test_import_resolved_the_cuda_math_libraries(self): + # Whatever provided them, the CUDA math libraries libcuvslam.so links against are loaded once cuvslam + # imports: this is the regression guard for wheels that neither bundle nor declare them. + import cuvslam # noqa: F401 (imported for its side effect on the process' loaded libraries) + with open('/proc/self/maps') as maps_file: + maps = maps_file.read() + for soname in ('libcublas.so.', 'libcusolver.so.', 'libcusparse.so.'): + self.assertIn(soname, maps) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/verify_pycuvslam_wheel_in_docker.sh b/scripts/verify_pycuvslam_wheel_in_docker.sh index e34ca86..b580284 100755 --- a/scripts/verify_pycuvslam_wheel_in_docker.sh +++ b/scripts/verify_pycuvslam_wheel_in_docker.sh @@ -7,6 +7,8 @@ if [ "$#" -ne 1 ] && [ "$#" -ne 3 ]; then echo " fresh environment and imports cuvslam, verifying the wheel filename is valid" echo " (pip-installable) and the auditwheel-repaired extension loads with the" echo " excluded CUDA libraries resolved from the system." + echo " Then reinstalls it with its cu12/cu13 extra in a clean environment and verifies that" + echo " the excluded CUDA libraries also resolve from the nvidia-* pip packages alone." echo " When expected_version and expected_git_sha are provided, also verifies that" echo " get_version() identifies that clean source revision without '-modified'." exit 1 @@ -42,6 +44,13 @@ fi WHEEL_NAME=$(basename "${WHEELS[0]}") +# Wheels are versioned +cu12/+cu13 by build_pycuvslam_in_docker.sh; the same tag names the extra +# that provides the CUDA math libraries excluded from the wheel. Empty for wheels built without the tag. +CUDA_EXTRA=$(echo "$WHEEL_NAME" | grep -oE "\+cu[0-9]+" | tr -d "+" || true) +if [ -z "$CUDA_EXTRA" ]; then + echo "Note: $WHEEL_NAME carries no +cuNN version tag; skipping the pip-provided CUDA libraries check." +fi + TTY_FLAG="" [ -t 0 ] && TTY_FLAG="-it" @@ -52,6 +61,7 @@ docker run --runtime=nvidia --gpus all --rm $TTY_FLAG --network host \ --user "$(id -u):$(id -g)" --group-add video -e HOME=/tmp \ -v "$OUTPUT_DIR:/output:ro" \ -e WHEEL_NAME="$WHEEL_NAME" \ + -e CUDA_EXTRA="$CUDA_EXTRA" \ -e EXPECTED_VERSION="$EXPECTED_VERSION" \ -e EXPECTED_GIT_SHA="$EXPECTED_GIT_SHA" \ cuvslam:local bash -c ' @@ -86,5 +96,45 @@ if expected_version: ) print("cuvslam wheel import OK, version:", version_info) +PY + + if [ -z "$CUDA_EXTRA" ]; then + exit 0 + fi + + # The wheel does not bundle cuBLAS/cuSOLVER/cuSPARSE, and this image provides them system-wide, so the check + # above cannot tell a wheel that declares them from one that silently depends on the CUDA Toolkit being + # installed. Install the wheel with its CUDA extra into a clean environment instead, and require that the + # libraries actually loaded are the pip-provided ones. + echo "--- Verifying the [$CUDA_EXTRA] extra provides the CUDA math libraries ---" + python3 -m venv /tmp/wheel_venv_cuda_extra + . /tmp/wheel_venv_cuda_extra/bin/activate + if ! pip install --no-cache-dir "/output/wheel/$WHEEL_NAME[$CUDA_EXTRA]" > /tmp/pip_cuda_extra.log 2>&1; then + cat /tmp/pip_cuda_extra.log + if grep -q "No matching distribution" /tmp/pip_cuda_extra.log; then + echo "SKIPPED: nvidia-* $CUDA_EXTRA wheels are not published for $(uname -m)." + exit 0 + fi + exit 1 + fi + cd /tmp + python3 - < Date: Wed, 29 Jul 2026 11:30:30 +0200 Subject: [PATCH 2/5] [fix] Address review on the wheel CUDA math library fix Do not let a failed extra resolution pass wheel verification: "No matching distribution" is what a broken extras declaration looks like too. Decide from the architecture, before the install, that the nvidia-* math library wheels are x86_64-only, and let every pip failure fail the job. Run the import-time preload regression guard in a subprocess. The test module imported cuvslam at module level, which ran preload() before the assertion, so the guard could not fail. Load _cuda_libs.py standalone for the helper tests. State in README.md that a CUDA Toolkit installation and the wheel's cu12/cu13 extra are alternatives, and drop "pip install cuvslam[cu12]", which resolves against PyPI rather than the downloaded wheel. --- README.md | 18 ++++----- python/test/test_cuda_libs.py | 44 +++++++++++++++++---- scripts/verify_pycuvslam_wheel_in_docker.sh | 20 ++++++---- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 56e5366..3bd2947 100644 --- a/README.md +++ b/README.md @@ -121,16 +121,13 @@ Only the combinations listed above are provided as pre-built wheels. The `cp312- and are compatible with Python 3.12 and later. Other Python, CUDA, or Jetson combinations require an [installation from source](#install-from-source). -**Prerequisite**: [CUDA Toolkit 12 or 13](https://developer.nvidia.com/cuda/toolkit) must be installed separately -(not included in the wheels). Its major version must match the wheel's `cu12` or `cu13` tag. +**Prerequisite**: the wheels link against CUDA and the CUDA math libraries (cuBLAS, cuSOLVER, cuSPARSE) but do not +bundle them, so the environment has to provide them in one of two ways: -The wheels link against the CUDA math libraries (cuBLAS, cuSOLVER, cuSPARSE) but do not bundle them, so they must come -from that CUDA Toolkit installation. On systems without one, install the extra matching the wheel's tag instead and pip -provides them: - -```bash -pip install "cuvslam[cu12]" # or "cuvslam[cu13]" -``` +- a [CUDA Toolkit 12 or 13](https://developer.nvidia.com/cuda/toolkit) installation whose major version matches the + wheel's `cu12` or `cu13` tag, or +- the wheel's matching `cu12`/`cu13` extra, which pulls the math libraries in as `nvidia-*` pip packages + (see step 3 below). A CUDA driver is still required; only the toolkit installation is not. Official wheels include cuNLS support for `Multisensor` mode; no separate cuNLS installation is required. @@ -143,7 +140,8 @@ To install (virtual environment is recommended): ```bash pip install cuvslam-*.whl -# ...or, without a CUDA Toolkit installation, with the CUDA math libraries from pip: +# ...or, without a CUDA Toolkit installation, with the CUDA math libraries from pip +# (use the extra matching the wheel's tag: cu12 or cu13): pip install "$(echo cuvslam-*.whl)[cu12]" ``` diff --git a/python/test/test_cuda_libs.py b/python/test/test_cuda_libs.py index 919253c..c01813b 100644 --- a/python/test/test_cuda_libs.py +++ b/python/test/test_cuda_libs.py @@ -12,11 +12,41 @@ # By using, reproducing, modifying, distributing, performing, or displaying any portion or element # of the software or derivative works thereof, you agree to be bound by this License. +import importlib.util import os +import subprocess +import sys import tempfile import unittest -from cuvslam import _cuda_libs + +def load_cuda_libs(): + """Load ``cuvslam/_cuda_libs.py`` as a standalone module, without importing ``cuvslam``. + + Importing the package runs ``preload()`` and loads the extension module, which is exactly the side effect + test_import_resolved_the_cuda_math_libraries has to observe in an interpreter that has not done it yet. + """ + package_root = importlib.util.find_spec('cuvslam').submodule_search_locations[0] + spec = importlib.util.spec_from_file_location('_cuda_libs_under_test', + os.path.join(package_root, '_cuda_libs.py')) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +_cuda_libs = load_cuda_libs() + +# Run in a fresh interpreter by test_import_resolved_the_cuda_math_libraries. +IMPORT_SIDE_EFFECT_PROBE = """ +import cuvslam # noqa: F401 (imported for its side effect on the process' loaded libraries) + +with open('/proc/self/maps') as maps_file: + maps = maps_file.read() + +missing = [soname for soname in ('libcublas.so.', 'libcusolver.so.', 'libcusparse.so.') if soname not in maps] +if missing: + raise SystemExit('not loaded after "import cuvslam": ' + ', '.join(missing)) +""" def make_pip_cuda_layout(root, components): @@ -72,12 +102,12 @@ def test_unloadable_libraries_do_not_raise(self): def test_import_resolved_the_cuda_math_libraries(self): # Whatever provided them, the CUDA math libraries libcuvslam.so links against are loaded once cuvslam - # imports: this is the regression guard for wheels that neither bundle nor declare them. - import cuvslam # noqa: F401 (imported for its side effect on the process' loaded libraries) - with open('/proc/self/maps') as maps_file: - maps = maps_file.read() - for soname in ('libcublas.so.', 'libcusolver.so.', 'libcusparse.so.'): - self.assertIn(soname, maps) + # imports: this is the regression guard for wheels that neither bundle nor declare them. It runs in a + # fresh interpreter, because a cuvslam already imported by another test would load them regardless of + # whether importing it still does. + probe = subprocess.run([sys.executable, '-c', IMPORT_SIDE_EFFECT_PROBE], + capture_output=True, text=True, check=False) + self.assertEqual(probe.returncode, 0, probe.stderr) if __name__ == "__main__": diff --git a/scripts/verify_pycuvslam_wheel_in_docker.sh b/scripts/verify_pycuvslam_wheel_in_docker.sh index b580284..b98645f 100755 --- a/scripts/verify_pycuvslam_wheel_in_docker.sh +++ b/scripts/verify_pycuvslam_wheel_in_docker.sh @@ -51,6 +51,15 @@ if [ -z "$CUDA_EXTRA" ]; then echo "Note: $WHEEL_NAME carries no +cuNN version tag; skipping the pip-provided CUDA libraries check." fi +# NVIDIA publishes the nvidia-* CUDA math library wheels the extras declare for x86_64 only; on Jetson the libraries +# come from JetPack. Decide that here, from the architecture, rather than from how pip fails later: a resolver failure +# is what a broken extra declaration looks like too, and that must fail the verification. +HOST_ARCH=$(uname -m) +if [ -n "$CUDA_EXTRA" ] && [ "$HOST_ARCH" != "x86_64" ]; then + echo "SKIPPED: no pip-provided CUDA libraries check on $HOST_ARCH; nvidia-* $CUDA_EXTRA wheels are x86_64-only." + CUDA_EXTRA="" +fi + TTY_FLAG="" [ -t 0 ] && TTY_FLAG="-it" @@ -109,14 +118,9 @@ PY echo "--- Verifying the [$CUDA_EXTRA] extra provides the CUDA math libraries ---" python3 -m venv /tmp/wheel_venv_cuda_extra . /tmp/wheel_venv_cuda_extra/bin/activate - if ! pip install --no-cache-dir "/output/wheel/$WHEEL_NAME[$CUDA_EXTRA]" > /tmp/pip_cuda_extra.log 2>&1; then - cat /tmp/pip_cuda_extra.log - if grep -q "No matching distribution" /tmp/pip_cuda_extra.log; then - echo "SKIPPED: nvidia-* $CUDA_EXTRA wheels are not published for $(uname -m)." - exit 0 - fi - exit 1 - fi + # A resolution failure here means the extra does not describe an installable set of packages, which is the very + # thing this stage exists to catch, so it fails the verification. + pip install --no-cache-dir "/output/wheel/$WHEEL_NAME[$CUDA_EXTRA]" cd /tmp python3 - < Date: Wed, 29 Jul 2026 12:04:14 +0200 Subject: [PATCH 3/5] [fix] Correct the rationale for the x86_64-only CUDA extra check The nvidia-* CUDA math library wheels are published for aarch64 as well, so "not published there" was the wrong reason to skip the check. The real one is that the aarch64 wheels are SBSA builds while the aarch64 wheel matrix entries are Jetson, where CUDA comes from JetPack. Say that in the script, and point Jetson users in README.md at JetPack rather than at the extra. --- README.md | 3 ++- scripts/verify_pycuvslam_wheel_in_docker.sh | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3bd2947..c81fdd6 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,8 @@ bundle them, so the environment has to provide them in one of two ways: - a [CUDA Toolkit 12 or 13](https://developer.nvidia.com/cuda/toolkit) installation whose major version matches the wheel's `cu12` or `cu13` tag, or - the wheel's matching `cu12`/`cu13` extra, which pulls the math libraries in as `nvidia-*` pip packages - (see step 3 below). A CUDA driver is still required; only the toolkit installation is not. + (see step 3 below). A CUDA driver is still required; only the toolkit installation is not. This is meant for x86_64 + systems without a toolkit — on Jetson, CUDA comes with JetPack, so use that rather than the extra. Official wheels include cuNLS support for `Multisensor` mode; no separate cuNLS installation is required. diff --git a/scripts/verify_pycuvslam_wheel_in_docker.sh b/scripts/verify_pycuvslam_wheel_in_docker.sh index b98645f..fc12527 100755 --- a/scripts/verify_pycuvslam_wheel_in_docker.sh +++ b/scripts/verify_pycuvslam_wheel_in_docker.sh @@ -51,12 +51,13 @@ if [ -z "$CUDA_EXTRA" ]; then echo "Note: $WHEEL_NAME carries no +cuNN version tag; skipping the pip-provided CUDA libraries check." fi -# NVIDIA publishes the nvidia-* CUDA math library wheels the extras declare for x86_64 only; on Jetson the libraries -# come from JetPack. Decide that here, from the architecture, rather than from how pip fails later: a resolver failure -# is what a broken extra declaration looks like too, and that must fail the verification. +# The extras target x86_64 systems without a CUDA Toolkit. The aarch64 entries in the wheel matrix are Jetson, where +# CUDA comes from JetPack and the aarch64 nvidia-* wheels are server-ARM (SBSA) builds, so installing those over a +# Tegra CUDA is not the configuration this check certifies. Decide that from the architecture, before touching pip: a +# resolver failure is what a broken extras declaration looks like too, and that must fail the verification. HOST_ARCH=$(uname -m) if [ -n "$CUDA_EXTRA" ] && [ "$HOST_ARCH" != "x86_64" ]; then - echo "SKIPPED: no pip-provided CUDA libraries check on $HOST_ARCH; nvidia-* $CUDA_EXTRA wheels are x86_64-only." + echo "SKIPPED: no pip-provided CUDA libraries check on $HOST_ARCH; the $CUDA_EXTRA extra targets x86_64 systems." CUDA_EXTRA="" fi From 6e8011de7074c656ad0aae9fa0900fce67474aaf Mon Sep 17 00:00:00 2001 From: "behnam.asadi" Date: Wed, 29 Jul 2026 14:16:01 +0200 Subject: [PATCH 4/5] [fix] Address the second review round on the wheel CUDA library fix Verify the pip provenance of every CUDA component the package declares, not just cuBLAS/cuSOLVER/cuSPARSE. The stage runs in a CUDA devel image, so had nvidia-nvjitlink-* been dropped from the extras, cuSOLVER and cuSPARSE would still have loaded by resolving nvJitLink from the image and the check would have passed for a wheel that fails without a toolkit. Deriving the set from _cuda_libs.CUDA_COMPONENTS covers future components without a second edit here, and resolving nvidia_root() the way preload() does asserts against the path production actually uses. Show both install extras in README.md. Every wheel declares both, since a wheel cannot vary its metadata by CUDA major, so a mismatched extra is not an error and installs math libraries of the wrong major silently. --- README.md | 8 +++++--- scripts/verify_pycuvslam_wheel_in_docker.sh | 11 ++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c81fdd6..b4610a7 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,11 @@ To install (virtual environment is recommended): ```bash pip install cuvslam-*.whl -# ...or, without a CUDA Toolkit installation, with the CUDA math libraries from pip -# (use the extra matching the wheel's tag: cu12 or cu13): -pip install "$(echo cuvslam-*.whl)[cu12]" +# ...or, without a CUDA Toolkit installation, with the CUDA math libraries from pip. Every wheel carries both +# extras, so pip accepts a mismatched one without an error and installs math libraries of the wrong CUDA major: +# pick the line matching the tag of the wheel downloaded above. +pip install "$(echo cuvslam-*.whl)[cu12]" # cu12 wheels +pip install "$(echo cuvslam-*.whl)[cu13]" # cu13 wheels ``` If a pre-built wheel is not available for your system, see [Install from Source](#install-from-source) below. diff --git a/scripts/verify_pycuvslam_wheel_in_docker.sh b/scripts/verify_pycuvslam_wheel_in_docker.sh index fc12527..8932c5a 100755 --- a/scripts/verify_pycuvslam_wheel_in_docker.sh +++ b/scripts/verify_pycuvslam_wheel_in_docker.sh @@ -126,17 +126,22 @@ PY python3 - < Date: Wed, 29 Jul 2026 16:01:44 +0200 Subject: [PATCH 5/5] [fix] Derive the README install extra from the wheel tag Two active pip commands, one per CUDA major, meant copying the block whole installed both extras and so both sets of math libraries. Reading the extra off the wheel's own +cu12/+cu13 tag leaves one runnable command that cannot be mismatched, using the same idiom the wheel verification script already uses to pick the extra. --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b4610a7..794dbd8 100644 --- a/README.md +++ b/README.md @@ -141,11 +141,12 @@ To install (virtual environment is recommended): ```bash pip install cuvslam-*.whl -# ...or, without a CUDA Toolkit installation, with the CUDA math libraries from pip. Every wheel carries both -# extras, so pip accepts a mismatched one without an error and installs math libraries of the wrong CUDA major: -# pick the line matching the tag of the wheel downloaded above. -pip install "$(echo cuvslam-*.whl)[cu12]" # cu12 wheels -pip install "$(echo cuvslam-*.whl)[cu13]" # cu13 wheels +# ...or, without a CUDA Toolkit installation, with the CUDA math libraries from pip. The extra has to match the +# wheel's CUDA major: every wheel declares both cu12 and cu13, so pip accepts a mismatched one without an error +# and installs math libraries of the wrong major. Reading the extra off the wheel's own +cu12/+cu13 tag rather +# than typing it keeps the two in step: +wheel=$(echo cuvslam-*.whl) +pip install "$wheel[$(echo "$wheel" | grep -oE '\+cu[0-9]+' | tr -d '+')]" ``` If a pre-built wheel is not available for your system, see [Install from Source](#install-from-source) below.