Skip to content

Commit d9da442

Browse files
atiaomar1978-hubcursoragentisVoid
authored
fix(cuda.core): fall back to driver when nvJitLink < 12.3 is installed (#2409)
* fix(cuda.core): fall back to driver when nvJitLink < 12.3 is installed Stop probing nvJitLink availability via module.version(), which calls the unversioned nvJitLinkVersion symbol missing in nvJitLink 12.0-12.2. Use symbol pointer inspection via _nvjitlink_has_version_symbol() instead, restoring cuda-core 0.6.0 fallback behavior. Fixes #2408 Signed-off-by: Omar Atie <atiaomar1978-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * test(cuda.core): add coverage for nvJitLink <12.3 driver fallback Add regression tests for Linker.which_backend() and _decide_nvjitlink_or_driver() when the nvJitLinkVersion symbol is missing (nvJitLink 12.0-12.2). Related to #2408 Signed-off-by: Omar Atie <atiaomar1978-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * docs(cuda.core): add 1.2.0 release note for nvJitLink <12.3 fallback fix Document the #2408 regression fix in the cuda.core 1.2.0 release notes. Related to #2408 Signed-off-by: Omar Atie <atiaomar1978-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cuda.core): probe nvJitLink version under DynamicLibNotFoundError guard Address review feedback: keep the >=12.3 version-symbol check inside _optional_cuda_import's probe so a missing nvJitLink dylib still falls back to cuLink. Continue avoiding module.version(), which raises FunctionNotFoundError on nvJitLink 12.0-12.2 (#2408). Add coverage for missing-dylib fallback and a guard that the probe does not call module.version(). Signed-off-by: Omar Atie <atiaomar1978-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cuda.core): use explicit try/except for nvJitLink version probe Address review feedback: drop the probe side-effect and catch DynamicLibNotFoundError around _nvjitlink_has_version_symbol so missing dylibs still fall back to cuLink. Keep avoiding module.version() for nvJitLink <12.3 (#2408). Mark newly added tests with agent_authored authorship markers. Signed-off-by: Omar Atie <atiaomar1978-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cuda.core): drop obsolete nvJitLink probe comments Signed-off-by: Omar Atie <atiaomar1978-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Signed-off-by: Omar Atie <atiaomar1978-hub@users.noreply.github.com> Co-authored-by: Omar Atie <atiaomar1978-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Michael Wang <13521008+isVoid@users.noreply.github.com>
1 parent fcde4b1 commit d9da442

4 files changed

Lines changed: 153 additions & 13 deletions

File tree

‎cuda_core/cuda/core/_linker.pyx‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ from dataclasses import dataclass
2929
from typing import TYPE_CHECKING, Union
3030
from warnings import warn
3131

32+
from cuda.pathfinder import DynamicLibNotFoundError
3233
from cuda.pathfinder._optional_cuda_import import _optional_cuda_import
3334
from cuda.core._device import Device
3435
from cuda.core._module import ObjectCode
@@ -683,22 +684,26 @@ def _decide_nvjitlink_or_driver() -> bool:
683684
" For best results, consider upgrading to a recent version of"
684685
)
685686

686-
nvjitlink_module = _optional_cuda_import(
687-
"cuda.bindings.nvjitlink",
688-
probe_function=lambda module: module.version(), # probe triggers nvJitLink runtime load
689-
)
687+
nvjitlink_module = _optional_cuda_import("cuda.bindings.nvjitlink")
690688
if nvjitlink_module is None:
691689
warn_txt = f"cuda.bindings.nvjitlink is not available, therefore {warn_txt_common} cuda-bindings."
692690
else:
693691
from cuda.bindings._internal import nvjitlink
694692

695-
if _nvjitlink_has_version_symbol(nvjitlink):
696-
_use_nvjitlink_backend = True
697-
return False # Use nvjitlink
698-
warn_txt = (
699-
f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is too old (<12.3)."
700-
f" Therefore cuda.bindings.nvjitlink is not usable and {warn_txt_common} nvJitLink."
701-
)
693+
try:
694+
has_version_symbol = _nvjitlink_has_version_symbol(nvjitlink)
695+
except DynamicLibNotFoundError:
696+
warn_txt = (
697+
f"cuda.bindings.nvjitlink is not available, therefore {warn_txt_common} cuda-bindings."
698+
)
699+
else:
700+
if has_version_symbol:
701+
_use_nvjitlink_backend = True
702+
return False # Use nvjitlink
703+
warn_txt = (
704+
f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is too old (<12.3)."
705+
f" Therefore cuda.bindings.nvjitlink is not usable and {warn_txt_common} nvJitLink."
706+
)
702707

703708
warn(warn_txt, stacklevel=2, category=RuntimeWarning)
704709
_driver = driver

‎cuda_core/docs/source/release/1.2.0-notes.rst‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ Fixes and enhancements
2525
versions 12.2 or newer.
2626
(`#2352 <https://github.com/NVIDIA/cuda-python/issues/2352>`__)
2727

28+
- :meth:`Linker.which_backend` and constructing a :class:`Linker` no longer
29+
raise ``FunctionNotFoundError`` when an nvJitLink older than 12.3
30+
(12.0–12.2) is installed. These versions do not export the unversioned
31+
``nvJitLinkVersion`` symbol, so probing the version crashed instead of
32+
falling back. ``cuda.core`` now warns and falls back to the driver
33+
(``cuLink``) backend, restoring the pre-0.7.0 behavior.
34+
(`#2409 <https://github.com/NVIDIA/cuda-python/pull/2409>`__,
35+
closes `#2408 <https://github.com/NVIDIA/cuda-python/issues/2408>`__)
36+
2837
Deprecation Notices
2938
-------------------
3039

‎cuda_core/tests/test_linker.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,49 @@ def fake_decide():
303303
assert result == "nvJitLink"
304304
assert called, "_decide_nvjitlink_or_driver was not called"
305305

306+
@pytest.mark.agent_authored(model="grok-4.5")
307+
def test_which_backend_falls_back_when_nvjitlink_too_old(self, monkeypatch):
308+
"""Regression test for #2408: old nvJitLink must not crash which_backend()."""
309+
monkeypatch.setattr(_linker, "_use_nvjitlink_backend", None)
310+
monkeypatch.setattr(_linker, "_driver", None)
311+
312+
def fake__optional_cuda_import(modname, probe_function=None):
313+
assert modname == "cuda.bindings.nvjitlink"
314+
assert probe_function is None
315+
return object()
316+
317+
monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import)
318+
monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: False)
319+
320+
with pytest.warns(RuntimeWarning, match="too old \\(<12.3\\)"):
321+
assert Linker.which_backend() == "driver"
322+
323+
assert _linker._use_nvjitlink_backend is False
324+
325+
@pytest.mark.agent_authored(model="grok-4.5")
326+
def test_which_backend_falls_back_when_dylib_missing(self, monkeypatch):
327+
"""Missing nvJitLink dylib must fall back without raising."""
328+
from cuda.pathfinder import DynamicLibNotFoundError
329+
330+
monkeypatch.setattr(_linker, "_use_nvjitlink_backend", None)
331+
monkeypatch.setattr(_linker, "_driver", None)
332+
333+
def raise_missing(_nvjitlink):
334+
raise DynamicLibNotFoundError("missing")
335+
336+
def fake__optional_cuda_import(modname, probe_function=None):
337+
assert modname == "cuda.bindings.nvjitlink"
338+
assert probe_function is None
339+
return object()
340+
341+
monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", raise_missing)
342+
monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import)
343+
344+
with pytest.warns(RuntimeWarning, match="cuda.bindings.nvjitlink is not available"):
345+
assert Linker.which_backend() == "driver"
346+
347+
assert _linker._use_nvjitlink_backend is False
348+
306349
def test_which_backend_is_classmethod(self):
307350
attr = inspect.getattr_static(Linker, "which_backend")
308351
assert isinstance(attr, classmethod)

‎cuda_core/tests/test_optional_dependency_imports.py‎

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
from cuda.core import _linker, _program
8+
from cuda.pathfinder import DynamicLibNotFoundError
89

910

1011
@pytest.fixture(autouse=True)
@@ -78,7 +79,7 @@ def fake__optional_cuda_import(modname, probe_function=None):
7879
def test_decide_nvjitlink_or_driver_reraises_nested_module_not_found(monkeypatch):
7980
def fake__optional_cuda_import(modname, probe_function=None):
8081
assert modname == "cuda.bindings.nvjitlink"
81-
assert probe_function is not None
82+
assert probe_function is None
8283
err = ModuleNotFoundError("No module named 'not_a_real_dependency'")
8384
err.name = "not_a_real_dependency"
8485
raise err
@@ -93,7 +94,7 @@ def fake__optional_cuda_import(modname, probe_function=None):
9394
def test_decide_nvjitlink_or_driver_falls_back_when_module_missing(monkeypatch):
9495
def fake__optional_cuda_import(modname, probe_function=None):
9596
assert modname == "cuda.bindings.nvjitlink"
96-
assert probe_function is not None
97+
assert probe_function is None
9798
return None
9899

99100
monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import)
@@ -103,3 +104,85 @@ def fake__optional_cuda_import(modname, probe_function=None):
103104

104105
assert use_driver_backend is True
105106
assert _linker._use_nvjitlink_backend is False
107+
108+
109+
@pytest.mark.agent_authored(model="grok-4.5")
110+
def test_decide_nvjitlink_or_driver_falls_back_when_dylib_missing(monkeypatch):
111+
"""Missing nvJitLink dylib must fall back via DynamicLibNotFoundError."""
112+
113+
def raise_missing(_nvjitlink):
114+
raise DynamicLibNotFoundError("libnvJitLink missing")
115+
116+
def fake__optional_cuda_import(modname, probe_function=None):
117+
assert modname == "cuda.bindings.nvjitlink"
118+
assert probe_function is None
119+
return object()
120+
121+
monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import)
122+
monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", raise_missing)
123+
124+
with pytest.warns(RuntimeWarning, match="cuda.bindings.nvjitlink is not available"):
125+
use_driver_backend = _linker._decide_nvjitlink_or_driver()
126+
127+
assert use_driver_backend is True
128+
assert _linker._use_nvjitlink_backend is False
129+
130+
131+
@pytest.mark.agent_authored(model="grok-4.5")
132+
def test_decide_nvjitlink_or_driver_falls_back_when_nvjitlink_too_old(monkeypatch):
133+
def fake__optional_cuda_import(modname, probe_function=None):
134+
assert modname == "cuda.bindings.nvjitlink"
135+
assert probe_function is None
136+
return object()
137+
138+
monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import)
139+
monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: False)
140+
141+
with pytest.warns(RuntimeWarning, match="too old \\(<12.3\\)"):
142+
use_driver_backend = _linker._decide_nvjitlink_or_driver()
143+
144+
assert use_driver_backend is True
145+
assert _linker._use_nvjitlink_backend is False
146+
147+
148+
@pytest.mark.agent_authored(model="grok-4.5")
149+
def test_decide_nvjitlink_or_driver_selects_nvjitlink_when_version_symbol_present(monkeypatch):
150+
def fake__optional_cuda_import(modname, probe_function=None):
151+
assert modname == "cuda.bindings.nvjitlink"
152+
assert probe_function is None
153+
return object()
154+
155+
monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import)
156+
monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: True)
157+
158+
use_driver_backend = _linker._decide_nvjitlink_or_driver()
159+
160+
assert use_driver_backend is False
161+
assert _linker._use_nvjitlink_backend is True
162+
163+
164+
@pytest.mark.agent_authored(model="grok-4.5")
165+
def test_decide_nvjitlink_or_driver_does_not_call_version(monkeypatch):
166+
"""Regression guard for #2408: must not call module.version()."""
167+
called = {"version": False, "inspect": False}
168+
169+
class FakeModule:
170+
def version(self):
171+
called["version"] = True
172+
raise AssertionError("module.version() must not be used for nvJitLink probing")
173+
174+
def fake_has_version(_nvjitlink):
175+
called["inspect"] = True
176+
return True
177+
178+
def fake__optional_cuda_import(modname, probe_function=None):
179+
assert modname == "cuda.bindings.nvjitlink"
180+
assert probe_function is None
181+
return FakeModule()
182+
183+
monkeypatch.setattr(_linker, "_optional_cuda_import", fake__optional_cuda_import)
184+
monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", fake_has_version)
185+
186+
assert _linker._decide_nvjitlink_or_driver() is False
187+
assert called["inspect"] is True
188+
assert called["version"] is False

0 commit comments

Comments
 (0)