Skip to content

Commit e83d434

Browse files
authored
Check nvJitLink driver major compatibility (#2320)
* Check nvJitLink driver major compatibility * Narrow nvJitLink driver compatibility fallback * Fix cuLink state storage lifetimes * Isolate nvJitLink-specific cache tests * Use public linker backend query in tests * Use direct backend skips in program cache tests --------- Co-authored-by: Michael Wang <isVoid@users.noreply.github.com>
1 parent c23833b commit e83d434

6 files changed

Lines changed: 167 additions & 36 deletions

File tree

cuda_core/cuda/core/_linker.pxd

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from libcpp.vector cimport vector
6+
7+
from cuda.bindings cimport cydriver
8+
59
from ._resource_handles cimport NvJitLinkHandle, CuLinkHandle
610

711

812
cdef class Linker:
913
cdef:
1014
NvJitLinkHandle _nvjitlink_handle
1115
CuLinkHandle _culink_handle
16+
# The driver retains these arrays until cuLinkDestroy. Declare them
17+
# after the handle so their destructors run after cuLinkDestroy.
18+
vector[cydriver.CUjit_option] _drv_jit_keys
19+
vector[void*] _drv_jit_values
1220
bint _use_nvjitlink
13-
object _drv_log_bufs # formatted_options list (driver); None for nvjitlink; cleared in link()
21+
object _drv_log_bufs # formatted_options list (driver); None for nvjitlink
1422
str _info_log # decoded log; None until link() or pre-link get_*_log()
1523
str _error_log # decoded log; None until link() or pre-link get_*_log()
1624
object _options # LinkerOptions

cuda_core/cuda/core/_linker.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ class LinkerOptions:
112112
113113
Since the linker may choose to use nvJitLink or the driver APIs as the linking backend,
114114
not all options are applicable. When the system's installed nvJitLink is too old (<12.3),
115-
or not installed, the driver APIs (cuLink) will be used instead.
115+
not installed, or older than the CUDA driver major version, the driver APIs (cuLink)
116+
will be used instead.
116117
117118
Attributes
118119
----------

cuda_core/cuda/core/_linker.pyx

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ from cuda.core._utils.cuda_utils import (
3939
driver,
4040
is_sequence,
4141
)
42+
from cuda.core._utils.version import driver_version
4243
from cuda.core.typing import CompilerBackendType, ObjectCodeFormatType
4344

4445
if TYPE_CHECKING:
@@ -52,7 +53,6 @@ _keep_driver_in_stub: "cuda.bindings.driver.CUlinkState"
5253
_keep_nvjitlink_in_stub: "cuda.bindings.nvjitlink.nvJitLinkHandle"
5354

5455
ctypedef const char* const_char_ptr
55-
ctypedef void* void_ptr
5656

5757
__all__ = ["Linker", "LinkerOptions"]
5858

@@ -155,10 +155,21 @@ cdef class Linker:
155155

156156
def close(self) -> None:
157157
"""Destroy this linker."""
158+
cdef vector[cydriver.CUjit_option] empty_keys
159+
cdef vector[void*] empty_values
158160
if self._use_nvjitlink:
159161
self._nvjitlink_handle.reset()
160162
else:
163+
if self._drv_log_bufs is not None:
164+
if self._info_log is None:
165+
self._info_log = self.get_info_log()
166+
if self._error_log is None:
167+
self._error_log = self.get_error_log()
168+
# Destroy the CUlinkState before releasing storage referenced by it.
161169
self._culink_handle.reset()
170+
self._drv_jit_keys.swap(empty_keys)
171+
self._drv_jit_values.swap(empty_values)
172+
self._drv_log_bufs = None
162173

163174
@property
164175
def handle(self) -> LinkerHandleT:
@@ -207,7 +218,8 @@ class LinkerOptions:
207218

208219
Since the linker may choose to use nvJitLink or the driver APIs as the linking backend,
209220
not all options are applicable. When the system's installed nvJitLink is too old (<12.3),
210-
or not installed, the driver APIs (cuLink) will be used instead.
221+
not installed, or older than the CUDA driver major version, the driver APIs (cuLink)
222+
will be used instead.
211223

212224
Attributes
213225
----------
@@ -473,8 +485,8 @@ cdef inline int Linker_init(Linker self, tuple object_codes, object options) exc
473485
cdef cydriver.CUlinkState c_raw_culink
474486
cdef Py_ssize_t c_num_opts, i
475487
cdef vector[const_char_ptr] c_str_opts
476-
cdef vector[cydriver.CUjit_option] c_jit_keys
477-
cdef vector[void_ptr] c_jit_values
488+
cdef cydriver.CUjit_option* c_drv_jit_keys_ptr
489+
cdef void** c_drv_jit_values_ptr
478490

479491
self._options = options = check_or_create_options(LinkerOptions, options, "Linker options")
480492

@@ -496,19 +508,24 @@ cdef inline int Linker_init(Linker self, tuple object_codes, object options) exc
496508
# the driver writes into via raw pointers during linking operations.
497509
self._drv_log_bufs = formatted_options
498510
c_num_opts = len(option_keys)
499-
c_jit_keys.resize(c_num_opts)
500-
c_jit_values.resize(c_num_opts)
511+
self._drv_jit_keys.resize(c_num_opts)
512+
self._drv_jit_values.resize(c_num_opts)
501513
for i in range(c_num_opts):
502-
c_jit_keys[i] = <cydriver.CUjit_option><int>option_keys[i]
514+
self._drv_jit_keys[i] = <cydriver.CUjit_option><int>option_keys[i]
503515
val = formatted_options[i]
504516
if isinstance(val, bytearray):
505-
c_jit_values[i] = <void*>PyByteArray_AS_STRING(val)
517+
self._drv_jit_values[i] = <void*>PyByteArray_AS_STRING(val)
506518
else:
507-
c_jit_values[i] = <void*><intptr_t>int(val)
519+
self._drv_jit_values[i] = <void*><intptr_t>int(val)
520+
c_drv_jit_keys_ptr = self._drv_jit_keys.data()
521+
c_drv_jit_values_ptr = self._drv_jit_values.data()
508522
try:
509523
with nogil:
510524
HANDLE_RETURN(cydriver.cuLinkCreate(
511-
<unsigned int>c_num_opts, c_jit_keys.data(), c_jit_values.data(), &c_raw_culink))
525+
<unsigned int>c_num_opts,
526+
c_drv_jit_keys_ptr,
527+
c_drv_jit_values_ptr,
528+
&c_raw_culink))
512529
except CUDAError as e:
513530
Linker_annotate_error_log(self, e)
514531
raise
@@ -622,11 +639,10 @@ cdef inline object Linker_link(Linker self, str target_type):
622639
raise
623640
code = (<char*>c_cubin_out)[:c_output_size]
624641

625-
# Linking is complete; cache the decoded log strings and release
626-
# the driver's raw bytearray buffers (no longer written to).
642+
# Linking is complete; cache the decoded logs. cuLinkDestroy may still
643+
# dereference the raw log-buffer pointers, so retain them until close().
627644
self._info_log = self.get_info_log()
628645
self._error_log = self.get_error_log()
629-
self._drv_log_bufs = None
630646

631647
return ObjectCode._init(bytes(code), target_type, name=self._options.name)
632648

@@ -680,12 +696,22 @@ def _decide_nvjitlink_or_driver() -> bool:
680696
from cuda.bindings._internal import nvjitlink
681697

682698
if _nvjitlink_has_version_symbol(nvjitlink):
683-
_use_nvjitlink_backend = True
684-
return False # Use nvjitlink
685-
warn_txt = (
686-
f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is too old (<12.3)."
687-
f" Therefore cuda.bindings.nvjitlink is not usable and {warn_txt_common} nvJitLink."
688-
)
699+
nvjitlink_version = nvjitlink_module.version()
700+
driver_major = driver_version()[0]
701+
if driver_major <= nvjitlink_version[0]:
702+
_use_nvjitlink_backend = True
703+
return False # Use nvjitlink
704+
705+
warn_txt = (
706+
f"CUDA driver major version {driver_major} is newer than "
707+
f"nvJitLink major version {nvjitlink_version[0]}; therefore "
708+
f"{warn_txt_common} nvJitLink."
709+
)
710+
else:
711+
warn_txt = (
712+
f"{'nvJitLink*.dll' if sys.platform == 'win32' else 'libnvJitLink.so*'} is too old (<12.3)."
713+
f" Therefore cuda.bindings.nvjitlink is not usable and {warn_txt_common} nvJitLink."
714+
)
689715

690716
warn(warn_txt, stacklevel=2, category=RuntimeWarning)
691717
_use_nvjitlink_backend = False

cuda_core/tests/test_linker.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from cuda.core import Device, Linker, LinkerOptions, Program, ProgramOptions, _linker
1010
from cuda.core._module import ObjectCode
11+
from cuda.core._program import _can_load_generated_ptx
1112
from cuda.core._utils.cuda_utils import CUDAError
1213

1314
ARCH = "sm_" + "".join(f"{i}" for i in Device().compute_capability)
@@ -196,19 +197,12 @@ def test_linker_options_as_bytes_nvjitlink():
196197
assert "-maxrregcount=32" in options_str
197198

198199

199-
def test_linker_options_as_bytes_invalid_backend():
200+
@pytest.mark.parametrize("backend", ("invalid", "driver"))
201+
def test_linker_options_as_bytes_invalid_backend(backend):
200202
"""Test LinkerOptions.as_bytes() with invalid backend"""
201203
options = LinkerOptions(arch="sm_80")
202204
with pytest.raises(ValueError, match="only supports 'nvjitlink' backend"):
203-
options.as_bytes("invalid")
204-
205-
206-
@pytest.mark.skipif(not is_culink_backend, reason="driver backend test")
207-
def test_linker_options_as_bytes_driver_not_supported():
208-
"""Test that as_bytes() is not supported for driver backend"""
209-
options = LinkerOptions(arch="sm_80")
210-
with pytest.raises(RuntimeError, match="as_bytes\\(\\) only supports 'nvjitlink' backend"):
211-
options.as_bytes("driver")
205+
options.as_bytes(backend)
212206

213207

214208
def test_linker_logs_cached_after_link(compile_ptx_functions):
@@ -234,6 +228,47 @@ def test_linker_handle(compile_ptx_functions):
234228
assert int(handle) != 0
235229

236230

231+
@pytest.mark.agent_authored(model="gpt-5")
232+
@pytest.mark.skipif(not is_culink_backend, reason="driver backend regression test")
233+
def test_driver_linker_lifetime_no_heap_corruption(compile_ptx_functions):
234+
if not _can_load_generated_ptx():
235+
pytest.skip("PTX version too new for current driver")
236+
237+
linker = Linker(*compile_ptx_functions, options=LinkerOptions(arch=ARCH))
238+
linker.link("cubin")
239+
linker.close()
240+
del linker
241+
242+
obj_a = Program(kernel_a, "c++", ProgramOptions(relocatable_device_code=True)).compile("ptx")
243+
obj_b = Program(device_function_b, "c++", ProgramOptions(relocatable_device_code=True)).compile("ptx")
244+
obj_c = Program(device_function_c, "c++", ProgramOptions(relocatable_device_code=True)).compile("ptx")
245+
linker = Linker(obj_a, obj_b, obj_c, options=LinkerOptions(arch=ARCH))
246+
linker.link("cubin")
247+
linker.close()
248+
249+
250+
@pytest.mark.agent_authored(model="gpt-5")
251+
@pytest.mark.skipif(not is_culink_backend, reason="driver backend regression test")
252+
def test_driver_linker_preserves_error_log_after_close(init_cuda):
253+
if not _can_load_generated_ptx():
254+
pytest.skip("PTX version too new for current driver")
255+
256+
bad_kernel = """
257+
extern __device__ int Z();
258+
__global__ void A() { int r = Z(); }
259+
"""
260+
bad_obj = Program(bad_kernel, "c++", ProgramOptions(relocatable_device_code=True)).compile("ptx")
261+
linker = Linker(bad_obj, options=LinkerOptions(arch=ARCH))
262+
with pytest.raises(CUDAError):
263+
linker.link("cubin")
264+
265+
error_log = linker.get_error_log()
266+
assert error_log
267+
linker.close()
268+
assert linker.get_error_log() == error_log
269+
assert isinstance(linker.get_info_log(), str)
270+
271+
237272
@pytest.mark.skipif(is_culink_backend, reason="nvjitlink options only tested with nvjitlink backend")
238273
def test_linker_options_nvjitlink_options_as_str():
239274
"""_prepare_nvjitlink_options(as_bytes=False) returns plain strings."""

cuda_core/tests/test_optional_dependency_imports.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
from cuda.core import _linker, _program
88

99

10+
class FakeNvJitLinkModule:
11+
def __init__(self, version):
12+
self._version = version
13+
14+
def version(self):
15+
return self._version
16+
17+
1018
@pytest.fixture(autouse=True)
1119
def restore_optional_import_state():
1220
saved_nvvm_module = _program._nvvm_module
@@ -103,3 +111,43 @@ def fake__optional_cuda_import(modname, probe_function=None):
103111

104112
assert use_driver_backend is True
105113
assert _linker._use_nvjitlink_backend is False
114+
115+
116+
@pytest.mark.agent_authored(model="gpt-5")
117+
@pytest.mark.parametrize(
118+
("driver_version", "nvjitlink_version"),
119+
[
120+
((12, 8, 0), (12, 9)),
121+
((12, 9, 0), (13, 0)),
122+
],
123+
)
124+
def test_decide_nvjitlink_or_driver_uses_nvjitlink_when_driver_is_not_newer(
125+
monkeypatch, driver_version, nvjitlink_version
126+
):
127+
monkeypatch.setattr(
128+
_linker,
129+
"_optional_cuda_import",
130+
lambda *_args, **_kwargs: FakeNvJitLinkModule(nvjitlink_version),
131+
)
132+
monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: True)
133+
monkeypatch.setattr(_linker, "driver_version", lambda: driver_version)
134+
135+
assert _linker._decide_nvjitlink_or_driver() is False
136+
assert _linker._use_nvjitlink_backend is True
137+
138+
139+
@pytest.mark.agent_authored(model="gpt-5")
140+
def test_decide_nvjitlink_or_driver_falls_back_when_driver_is_newer(monkeypatch):
141+
monkeypatch.setattr(
142+
_linker,
143+
"_optional_cuda_import",
144+
lambda *_args, **_kwargs: FakeNvJitLinkModule((12, 9)),
145+
)
146+
monkeypatch.setattr(_linker, "_nvjitlink_has_version_symbol", lambda _nvjitlink: True)
147+
monkeypatch.setattr(_linker, "driver_version", lambda: (13, 0, 0))
148+
149+
with pytest.warns(RuntimeWarning, match="is newer than nvJitLink major version"):
150+
use_driver_backend = _linker._decide_nvjitlink_or_driver()
151+
152+
assert use_driver_backend is True
153+
assert _linker._use_nvjitlink_backend is False

cuda_core/tests/test_program_cache.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
import pytest
99

10+
from cuda.core import _linker
11+
12+
is_culink_backend = _linker._decide_nvjitlink_or_driver()
13+
1014

1115
def test_program_cache_resource_is_abstract():
1216
from cuda.core.utils import ProgramCacheResource
@@ -369,9 +373,6 @@ def test_make_program_cache_key_ignores_name_expressions_for_non_nvrtc(code_type
369373
{"link_time_optimization": None},
370374
id="lto_false_eq_none",
371375
),
372-
# ``time`` is a presence gate: the linker emits ``-time`` for any
373-
# non-None value, so True / "path" produce the same flag.
374-
pytest.param({"time": True}, {"time": "timing.csv"}, id="time_true_eq_path"),
375376
# ``no_cache`` has an ``is True`` gate; False and None equivalent.
376377
pytest.param({"no_cache": False}, {"no_cache": None}, id="no_cache_false_eq_none"),
377378
],
@@ -390,6 +391,18 @@ def test_make_program_cache_key_ptx_linker_equivalent_options_hash_same(a, b, mo
390391
assert k_a == k_b
391392

392393

394+
@pytest.mark.skipif(is_culink_backend, reason="test requires nvJitLink backend")
395+
@pytest.mark.agent_authored(model="gpt-5")
396+
def test_make_program_cache_key_ptx_nvjitlink_time_values_hash_same(monkeypatch):
397+
"""nvJitLink emits ``-time`` for any non-None value."""
398+
from cuda.core.utils import _program_cache
399+
400+
monkeypatch.setattr(_program_cache._keys, "_linker_backend_and_version", lambda _use_driver: ("nvJitLink", "12030"))
401+
k_true = _make_key(code=".version 7.0", code_type="ptx", options=_opts(time=True))
402+
k_path = _make_key(code=".version 7.0", code_type="ptx", options=_opts(time="timing.csv"))
403+
assert k_true == k_path
404+
405+
393406
@pytest.mark.parametrize(
394407
"field, a, b",
395408
[
@@ -999,9 +1012,9 @@ def test_make_program_cache_key_rejects_side_effect_options_nvrtc(option_kw, ext
9991012
pytest.param({"time": "whatever.csv"}, id="time_path"),
10001013
],
10011014
)
1015+
@pytest.mark.skipif(is_culink_backend, reason="test requires nvJitLink backend")
10021016
def test_make_program_cache_key_accepts_side_effect_options_for_ptx(option_kw):
1003-
"""The side-effect guard is NVRTC-specific: PTX (linker) and NVVM must
1004-
not be blocked by options whose side effects only apply under NVRTC."""
1017+
"""nvJitLink ``-time`` writes only to its info log, not the filesystem."""
10051018
_make_key(code=".version 7.0", code_type="ptx", options=_opts(**option_kw)) # no raise
10061019

10071020

0 commit comments

Comments
 (0)