diff --git a/cuda_core/cuda/core/_linker.pyx b/cuda_core/cuda/core/_linker.pyx index 39b1f010a9e..c30bc70b05f 100644 --- a/cuda_core/cuda/core/_linker.pyx +++ b/cuda_core/cuda/core/_linker.pyx @@ -29,6 +29,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Union from warnings import warn +from cuda.pathfinder import DynamicLibNotFoundError from cuda.pathfinder._optional_cuda_import import _optional_cuda_import from cuda.core._device import Device from cuda.core._module import ObjectCode @@ -683,22 +684,26 @@ def _decide_nvjitlink_or_driver() -> bool: " For best results, consider upgrading to a recent version of" ) - nvjitlink_module = _optional_cuda_import( - "cuda.bindings.nvjitlink", - probe_function=lambda module: module.version(), # probe triggers nvJitLink runtime load - ) + nvjitlink_module = _optional_cuda_import("cuda.bindings.nvjitlink") if nvjitlink_module is None: warn_txt = f"cuda.bindings.nvjitlink is not available, therefore {warn_txt_common} cuda-bindings." else: from cuda.bindings._internal import nvjitlink - if _nvjitlink_has_version_symbol(nvjitlink): - _use_nvjitlink_backend = True - return False # Use nvjitlink - warn_txt = ( - f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is too old (<12.3)." - f" Therefore cuda.bindings.nvjitlink is not usable and {warn_txt_common} nvJitLink." - ) + try: + has_version_symbol = _nvjitlink_has_version_symbol(nvjitlink) + except DynamicLibNotFoundError: + warn_txt = ( + f"cuda.bindings.nvjitlink is not available, therefore {warn_txt_common} cuda-bindings." + ) + else: + if has_version_symbol: + _use_nvjitlink_backend = True + return False # Use nvjitlink + warn_txt = ( + f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is too old (<12.3)." + f" Therefore cuda.bindings.nvjitlink is not usable and {warn_txt_common} nvJitLink." + ) warn(warn_txt, stacklevel=2, category=RuntimeWarning) _use_nvjitlink_backend = False diff --git a/cuda_core/docs/source/release/1.2.0-notes.rst b/cuda_core/docs/source/release/1.2.0-notes.rst index de255d01cbb..cf884ed64cc 100644 --- a/cuda_core/docs/source/release/1.2.0-notes.rst +++ b/cuda_core/docs/source/release/1.2.0-notes.rst @@ -25,6 +25,15 @@ Fixes and enhancements versions 12.2 or newer. (`#2352 `__) +- :meth:`Linker.which_backend` and constructing a :class:`Linker` no longer + raise ``FunctionNotFoundError`` when an nvJitLink older than 12.3 + (12.0–12.2) is installed. These versions do not export the unversioned + ``nvJitLinkVersion`` symbol, so probing the version crashed instead of + falling back. ``cuda.core`` now warns and falls back to the driver + (``cuLink``) backend, restoring the pre-0.7.0 behavior. + (`#2409 `__, + closes `#2408 `__) + Deprecation Notices ------------------- diff --git a/cuda_core/tests/test_linker.py b/cuda_core/tests/test_linker.py index 9d95b5fd9c3..4f4433a1a1a 100644 --- a/cuda_core/tests/test_linker.py +++ b/cuda_core/tests/test_linker.py @@ -303,6 +303,49 @@ def fake_decide(): assert result == "nvJitLink" assert called, "_decide_nvjitlink_or_driver was not called" + @pytest.mark.agent_authored(model="grok-4.5") + def test_which_backend_falls_back_when_nvjitlink_too_old(self, monkeypatch): + """Regression test for #2408: old nvJitLink must not crash which_backend().""" + monkeypatch.setattr(_linker, "_use_nvjitlink_backend", None) + monkeypatch.setattr(_linker, "_driver", None) + + def fake__optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvjitlink" + assert probe_function is None + return object() + + monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) + monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: False) + + with pytest.warns(RuntimeWarning, match="too old \\(<12.3\\)"): + assert Linker.which_backend() == "driver" + + assert _linker._use_nvjitlink_backend is False + + @pytest.mark.agent_authored(model="grok-4.5") + def test_which_backend_falls_back_when_dylib_missing(self, monkeypatch): + """Missing nvJitLink dylib must fall back without raising.""" + from cuda.pathfinder import DynamicLibNotFoundError + + monkeypatch.setattr(_linker, "_use_nvjitlink_backend", None) + monkeypatch.setattr(_linker, "_driver", None) + + def raise_missing(_nvjitlink): + raise DynamicLibNotFoundError("missing") + + def fake__optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvjitlink" + assert probe_function is None + return object() + + monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", raise_missing) + monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) + + with pytest.warns(RuntimeWarning, match="cuda.bindings.nvjitlink is not available"): + assert Linker.which_backend() == "driver" + + assert _linker._use_nvjitlink_backend is False + def test_which_backend_is_classmethod(self): attr = inspect.getattr_static(Linker, "which_backend") assert isinstance(attr, classmethod) diff --git a/cuda_core/tests/test_optional_dependency_imports.py b/cuda_core/tests/test_optional_dependency_imports.py index 02edcc9839a..9ba7358f9fe 100644 --- a/cuda_core/tests/test_optional_dependency_imports.py +++ b/cuda_core/tests/test_optional_dependency_imports.py @@ -5,6 +5,7 @@ import pytest from cuda.core import _linker, _program +from cuda.pathfinder import DynamicLibNotFoundError @pytest.fixture(autouse=True) @@ -78,7 +79,7 @@ def fake__optional_cuda_import(modname, probe_function=None): def test_decide_nvjitlink_or_driver_reraises_nested_module_not_found(monkeypatch): def fake__optional_cuda_import(modname, probe_function=None): assert modname == "cuda.bindings.nvjitlink" - assert probe_function is not None + assert probe_function is None err = ModuleNotFoundError("No module named 'not_a_real_dependency'") err.name = "not_a_real_dependency" raise err @@ -93,7 +94,7 @@ def fake__optional_cuda_import(modname, probe_function=None): def test_decide_nvjitlink_or_driver_falls_back_when_module_missing(monkeypatch): def fake__optional_cuda_import(modname, probe_function=None): assert modname == "cuda.bindings.nvjitlink" - assert probe_function is not None + assert probe_function is None return None monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) @@ -103,3 +104,85 @@ def fake__optional_cuda_import(modname, probe_function=None): assert use_driver_backend is True assert _linker._use_nvjitlink_backend is False + + +@pytest.mark.agent_authored(model="grok-4.5") +def test_decide_nvjitlink_or_driver_falls_back_when_dylib_missing(monkeypatch): + """Missing nvJitLink dylib must fall back via DynamicLibNotFoundError.""" + + def raise_missing(_nvjitlink): + raise DynamicLibNotFoundError("libnvJitLink missing") + + def fake__optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvjitlink" + assert probe_function is None + return object() + + monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) + monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", raise_missing) + + with pytest.warns(RuntimeWarning, match="cuda.bindings.nvjitlink is not available"): + use_driver_backend = _linker._decide_nvjitlink_or_driver() + + assert use_driver_backend is True + assert _linker._use_nvjitlink_backend is False + + +@pytest.mark.agent_authored(model="grok-4.5") +def test_decide_nvjitlink_or_driver_falls_back_when_nvjitlink_too_old(monkeypatch): + def fake__optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvjitlink" + assert probe_function is None + return object() + + monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) + monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: False) + + with pytest.warns(RuntimeWarning, match="too old \\(<12.3\\)"): + use_driver_backend = _linker._decide_nvjitlink_or_driver() + + assert use_driver_backend is True + assert _linker._use_nvjitlink_backend is False + + +@pytest.mark.agent_authored(model="grok-4.5") +def test_decide_nvjitlink_or_driver_selects_nvjitlink_when_version_symbol_present(monkeypatch): + def fake__optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvjitlink" + assert probe_function is None + return object() + + monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) + monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: True) + + use_driver_backend = _linker._decide_nvjitlink_or_driver() + + assert use_driver_backend is False + assert _linker._use_nvjitlink_backend is True + + +@pytest.mark.agent_authored(model="grok-4.5") +def test_decide_nvjitlink_or_driver_does_not_call_version(monkeypatch): + """Regression guard for #2408: must not call module.version().""" + called = {"version": False, "inspect": False} + + class FakeModule: + def version(self): + called["version"] = True + raise AssertionError("module.version() must not be used for nvJitLink probing") + + def fake_has_version(_nvjitlink): + called["inspect"] = True + return True + + def fake__optional_cuda_import(modname, probe_function=None): + assert modname == "cuda.bindings.nvjitlink" + assert probe_function is None + return FakeModule() + + monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import) + monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", fake_has_version) + + assert _linker._decide_nvjitlink_or_driver() is False + assert called["inspect"] is True + assert called["version"] is False