Skip to content

Commit 8b6e9f5

Browse files
authored
feat(cuda.core): support nvJitLink incremental linking (#2867)
1 parent f59229e commit 8b6e9f5

9 files changed

Lines changed: 463 additions & 23 deletions

File tree

cuda_core/cuda/core/_linker.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ cdef class Linker:
1818
vector[cydriver.CUjit_option] _drv_jit_keys
1919
vector[void*] _drv_jit_values
2020
bint _use_nvjitlink
21+
bint _has_ptx_or_cubin_input
2122
object _drv_log_bufs # formatted_options list (driver); None for nvjitlink
2223
str _info_log # decoded log; None until link() or pre-link get_*_log()
2324
str _error_log # decoded log; None until link() or pre-link get_*_log()

cuda_core/cuda/core/_linker.pyi

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ const_char_ptr: TypeAlias = bytes
2121
__all__ = ['Linker', 'LinkerOptions']
2222
LinkerHandleT = Union['cuda.bindings.nvjitlink.nvJitLinkHandle', 'cuda.bindings.driver.CUlinkState']
2323
_driver = None
24+
_nvjitlink = None
25+
_nvjitlink_version = None
2426
_inited = False
2527
_use_nvjitlink_backend = None
2628
_nvjitlink_input_types = None
@@ -50,7 +52,9 @@ class Linker:
5052
Parameters
5153
----------
5254
target_type : ObjectCodeFormatType | str
53-
The type of the target output. Must be either "cubin" or "ptx".
55+
The type of the target output. Must be "cubin", "ptx", or
56+
"ltoir". Linked LTOIR output requires
57+
``link_time_optimization=True`` and nvJitLink 13.3 or newer.
5458
5559
Returns
5660
-------
@@ -61,6 +65,16 @@ class Linker:
6165
6266
Ensure that input object codes were compiled with appropriate
6367
flags for linking (e.g., relocatable device code enabled).
68+
69+
A CUBIN produced with ``incremental=True`` can be passed directly
70+
to another :class:`Linker`, but it can still contain unresolved
71+
device references and should be finalized before execution.
72+
73+
``"ltoir"`` output contains only the LTOIR carried by the inputs.
74+
Direct PTX and CUBIN inputs are rejected because they carry no
75+
LTOIR. FATBIN, host object, and library inputs are accepted but
76+
may carry no LTOIR, in which case they contribute nothing to the
77+
output.
6478
"""
6579
def get_error_log(self) -> str:
6680
"""Get the error log generated by the linker.
@@ -137,6 +151,11 @@ class LinkerOptions:
137151
link_time_optimization : bool, optional
138152
Perform link time optimization.
139153
Default: False.
154+
incremental : bool, optional
155+
Perform an incremental link. The result can be passed
156+
directly to a later :class:`Linker`. Requires nvJitLink 13.2 or newer
157+
and is not supported by the driver linker backend.
158+
Default: False.
140159
ptx : bool, optional
141160
Emit PTX after linking instead of CUBIN; only supported with ``link_time_optimization=True``.
142161
Default: False.
@@ -216,6 +235,7 @@ class LinkerOptions:
216235
split_compile_extended: int | None = None
217236
no_cache: bool | None = None
218237
numba_debug: bool | None = None
238+
incremental: bool | None = None
219239

220240
def __post_init__(self) -> None: ...
221241
def _prepare_nvjitlink_options(self, as_bytes: bool=False) -> list[bytes] | list[str]: ...
@@ -241,6 +261,10 @@ class LinkerOptions:
241261
If nvJitLink backend is not available.
242262
"""
243263

264+
def _require_nvjitlink_version(minimum_version: tuple[int, int], feature: str) -> None:
265+
"""Check that the cached nvJitLink runtime meets a feature's requirement."""
266+
def _linked_ltoir_output_module():
267+
"""Return bindings that can retrieve linked LTOIR without a Cython dependency."""
244268
def _nvjitlink_has_version_symbol(nvjitlink) -> bool: ...
245269
def _decide_nvjitlink_or_driver() -> bool:
246270
"""Return True if falling back to the cuLink* driver APIs."""

cuda_core/cuda/core/_linker.pyx

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ from cuda.bindings cimport cynvjitlink
1818

1919
from ._rt cimport (
2020
as_cu,
21+
as_intptr,
2122
as_py,
2223
create_culink_handle,
2324
create_nvjitlink_handle,
@@ -101,7 +102,9 @@ cdef class Linker:
101102
Parameters
102103
----------
103104
target_type : ObjectCodeFormatType | str
104-
The type of the target output. Must be either "cubin" or "ptx".
105+
The type of the target output. Must be "cubin", "ptx", or
106+
"ltoir". Linked LTOIR output requires
107+
``link_time_optimization=True`` and nvJitLink 13.3 or newer.
105108

106109
Returns
107110
-------
@@ -112,6 +115,16 @@ cdef class Linker:
112115

113116
Ensure that input object codes were compiled with appropriate
114117
flags for linking (e.g., relocatable device code enabled).
118+
119+
A CUBIN produced with ``incremental=True`` can be passed directly
120+
to another :class:`Linker`, but it can still contain unresolved
121+
device references and should be finalized before execution.
122+
123+
``"ltoir"`` output contains only the LTOIR carried by the inputs.
124+
Direct PTX and CUBIN inputs are rejected because they carry no
125+
LTOIR. FATBIN, host object, and library inputs are accepted but
126+
may carry no LTOIR, in which case they contribute nothing to the
127+
output.
115128
"""
116129
Linker_check_open(self)
117130
return Linker_link(self, str(target_type))
@@ -256,6 +269,11 @@ class LinkerOptions:
256269
link_time_optimization : bool, optional
257270
Perform link time optimization.
258271
Default: False.
272+
incremental : bool, optional
273+
Perform an incremental link. The result can be passed
274+
directly to a later :class:`Linker`. Requires nvJitLink 13.2 or newer
275+
and is not supported by the driver linker backend.
276+
Default: False.
259277
ptx : bool, optional
260278
Emit PTX after linking instead of CUBIN; only supported with ``link_time_optimization=True``.
261279
Default: False.
@@ -336,6 +354,7 @@ class LinkerOptions:
336354
split_compile_extended: int | None = None
337355
no_cache: bool | None = None
338356
numba_debug: bool | None = None
357+
incremental: bool | None = None
339358

340359
def __post_init__(self) -> None:
341360
_lazy_init()
@@ -371,6 +390,8 @@ class LinkerOptions:
371390
options.append("-verbose")
372391
if self.link_time_optimization:
373392
options.append("-lto")
393+
if self.incremental:
394+
options.append("-r")
374395
if self.ptx:
375396
options.append("-ptx")
376397
if self.optimization_level is not None:
@@ -450,6 +471,8 @@ class LinkerOptions:
450471
if self.link_time_optimization:
451472
formatted_options.append(1)
452473
option_keys.append(_driver.CUjit_option.CU_JIT_LTO)
474+
if self.incremental:
475+
raise ValueError("incremental option is not supported by the driver API")
453476
if self.ptx:
454477
raise ValueError("ptx option is not supported by the driver API")
455478
if self.optimization_level is not None:
@@ -532,8 +555,13 @@ cdef inline int Linker_init(Linker self, tuple object_codes, object options) exc
532555
cdef void** c_drv_jit_values_ptr
533556

534557
self._options = options = check_or_create_options(LinkerOptions, options, "Linker options")
558+
self._has_ptx_or_cubin_input = False
559+
if options.incremental and options.ptx:
560+
raise ValueError("incremental and ptx output options cannot be used together")
535561

536562
if _use_nvjitlink_backend:
563+
if options.incremental:
564+
_require_nvjitlink_version((13, 2), "incremental linking")
537565
self._use_nvjitlink = True
538566
options_bytes = options._prepare_nvjitlink_options(as_bytes=True)
539567
c_num_opts = len(options_bytes)
@@ -639,17 +667,34 @@ cdef inline void Linker_add_code_object(Linker self, object object_code) except
639667
Linker_annotate_error_log(self, e)
640668
raise
641669

670+
if object_code.code_type in ("ptx", "cubin"):
671+
self._has_ptx_or_cubin_input = True
672+
642673

643674
cdef inline object Linker_link(Linker self, str target_type):
644675
"""Complete linking and return the result as ObjectCode."""
645-
if target_type not in ("cubin", "ptx"):
676+
if target_type not in ("cubin", "ptx", "ltoir"):
646677
raise ValueError(f"Unsupported target type: {target_type}")
678+
if self._options.incremental and target_type == "ptx":
679+
raise ValueError("PTX output is not supported for incremental linking")
680+
if target_type == "ltoir":
681+
if not self._use_nvjitlink:
682+
raise ValueError("LTOIR output is not supported by the driver API")
683+
if not self._options.link_time_optimization:
684+
raise ValueError("LTOIR output requires link_time_optimization=True")
685+
if self._has_ptx_or_cubin_input:
686+
raise ValueError(
687+
'LTOIR output is not supported with "ptx" or "cubin" inputs; '
688+
"they carry no LTOIR and would be omitted from the output"
689+
)
690+
nvjitlink_module = _linked_ltoir_output_module()
647691

648692
cdef cynvjitlink.nvJitLinkHandle c_nvjitlink_h
649693
cdef cydriver.CUlinkState c_culink_state
650694
cdef size_t c_output_size = 0
651695
cdef char* c_code_ptr
652696
cdef void* c_cubin_out = NULL
697+
cdef intptr_t c_handle
653698

654699
if self._use_nvjitlink:
655700
c_nvjitlink_h = as_cu(self._nvjitlink_handle)
@@ -663,14 +708,19 @@ cdef inline object Linker_link(Linker self, str target_type):
663708
with nogil:
664709
HANDLE_RETURN_NVJITLINK(c_nvjitlink_h,
665710
cynvjitlink.nvJitLinkGetLinkedCubin(c_nvjitlink_h, c_code_ptr))
666-
else:
711+
elif target_type == "ptx":
667712
HANDLE_RETURN_NVJITLINK(c_nvjitlink_h,
668713
cynvjitlink.nvJitLinkGetLinkedPtxSize(c_nvjitlink_h, &c_output_size))
669714
code = bytearray(c_output_size)
670715
c_code_ptr = <char*>(<bytearray>code)
671716
with nogil:
672717
HANDLE_RETURN_NVJITLINK(c_nvjitlink_h,
673718
cynvjitlink.nvJitLinkGetLinkedPtx(c_nvjitlink_h, c_code_ptr))
719+
else:
720+
c_handle = as_intptr(self._nvjitlink_handle)
721+
output_size = nvjitlink_module.get_linked_ltoir_size(c_handle)
722+
code = bytearray(output_size)
723+
nvjitlink_module.get_linked_ltoir(c_handle, code)
674724
else:
675725
c_culink_state = as_cu(self._culink_handle)
676726
try:
@@ -702,6 +752,8 @@ cdef inline void Linker_annotate_error_log(Linker self, object e):
702752

703753
# TODO: revisit this treatment for py313t builds
704754
_driver = None # populated if nvJitLink cannot be used
755+
_nvjitlink = None # populated if nvJitLink can be used
756+
_nvjitlink_version = None
705757
_inited = False
706758
_use_nvjitlink_backend = None # set by _decide_nvjitlink_or_driver()
707759

@@ -710,6 +762,31 @@ _nvjitlink_input_types = None
710762
_driver_input_types = None
711763

712764

765+
def _require_nvjitlink_version(minimum_version: tuple[int, int], feature: str) -> None:
766+
"""Check that the cached nvJitLink runtime meets a feature's requirement."""
767+
if _nvjitlink_version < minimum_version:
768+
required = ".".join(str(component) for component in minimum_version)
769+
detected = ".".join(str(component) for component in _nvjitlink_version)
770+
raise RuntimeError(f"{feature} requires nvJitLink {required} or newer; found {detected}")
771+
772+
773+
# TODO(#2783): Replace this Python-level dispatch with direct cimports once
774+
# the cuda-bindings runtime floor includes the linked-LTOIR getters.
775+
def _linked_ltoir_output_module():
776+
"""Return bindings that can retrieve linked LTOIR without a Cython dependency."""
777+
_require_nvjitlink_version((13, 3), "LTOIR output")
778+
missing = [
779+
name
780+
for name in ("get_linked_ltoir_size", "get_linked_ltoir")
781+
if not hasattr(_nvjitlink, name)
782+
]
783+
if missing:
784+
raise RuntimeError(
785+
"LTOIR output requires cuda-bindings with " + " and ".join(missing)
786+
)
787+
return _nvjitlink
788+
789+
713790
def _nvjitlink_has_version_symbol(nvjitlink) -> bool:
714791
# This condition is equivalent to testing for version >= 12.3
715792
return bool(nvjitlink._inspect_function_pointer("__nvJitLinkVersion"))
@@ -718,10 +795,13 @@ def _nvjitlink_has_version_symbol(nvjitlink) -> bool:
718795
# Note: this function is reused in the tests
719796
def _decide_nvjitlink_or_driver() -> bool:
720797
"""Return True if falling back to the cuLink* driver APIs."""
721-
global _driver, _use_nvjitlink_backend
798+
global _driver, _nvjitlink, _nvjitlink_version, _use_nvjitlink_backend
722799
if _use_nvjitlink_backend is not None:
723800
return not _use_nvjitlink_backend
724801

802+
_nvjitlink = None
803+
_nvjitlink_version = None
804+
725805
warn_txt_common = (
726806
"the driver APIs will be used instead, which do not support"
727807
" minor version compatibility or linking LTO IRs."
@@ -742,6 +822,9 @@ def _decide_nvjitlink_or_driver() -> bool:
742822
)
743823
else:
744824
if has_version_symbol:
825+
detected_version = nvjitlink_module.version()
826+
_nvjitlink = nvjitlink_module
827+
_nvjitlink_version = detected_version
745828
_use_nvjitlink_backend = True
746829
return False # Use nvjitlink
747830
warn_txt = (

cuda_core/cuda/core/_module.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -355,13 +355,13 @@ class ObjectCode:
355355
"""
356356
@staticmethod
357357
def from_object(module: bytes | str, *, name: str='', symbol_mapping: dict[str, str] | None=None) -> ObjectCode:
358-
"""Create an :class:`ObjectCode` instance from an existing object code.
358+
"""Create an :class:`ObjectCode` instance from a host object containing device code.
359359
360360
Parameters
361361
----------
362362
module : bytes | str
363-
Either a bytes object containing the in-memory object code to load, or
364-
a file path string pointing to the on-disk object code to load.
363+
Either a bytes object containing the in-memory host object to load, or
364+
a file path string pointing to the on-disk host object to load.
365365
name : str | None
366366
A human-readable identifier representing this code object.
367367
symbol_mapping : dict | None

cuda_core/cuda/core/_module.pyx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -741,13 +741,13 @@ cdef class ObjectCode:
741741

742742
@staticmethod
743743
def from_object(module: bytes | str, *, name: str = "", symbol_mapping: dict[str, str] | None = None) -> ObjectCode:
744-
"""Create an :class:`ObjectCode` instance from an existing object code.
744+
"""Create an :class:`ObjectCode` instance from a host object containing device code.
745745

746746
Parameters
747747
----------
748748
module : bytes | str
749-
Either a bytes object containing the in-memory object code to load, or
750-
a file path string pointing to the on-disk object code to load.
749+
Either a bytes object containing the in-memory host object to load, or
750+
a file path string pointing to the on-disk host object to load.
751751
name : str | None
752752
A human-readable identifier representing this code object.
753753
symbol_mapping : dict | None

cuda_core/cuda/core/typing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class ObjectCodeFormatType(StrEnum):
8484
* ``CUBIN`` — device-native CUDA binary.
8585
* ``LTOIR`` — LTO (link-time optimization) IR for later linking.
8686
* ``FATBIN`` — fat binary bundling multiple device images.
87-
* ``OBJECT`` — relocatable device object.
87+
* ``OBJECT`` — host object containing device code.
8888
* ``LIBRARY`` — device code library.
8989
"""
9090

cuda_core/docs/source/release/1.3.0-notes.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
New features
1010
------------
1111

12+
- Added incremental linking to :class:`Linker` through
13+
``LinkerOptions(incremental=True)`` on nvJitLink 13.2 or newer. A partial
14+
native result is returned as a CUBIN and can be passed directly to another
15+
linker. :meth:`Linker.link` also accepts ``"ltoir"`` on nvJitLink 13.3 or
16+
newer so incremental LTO chains can retain IR until their final link. Direct
17+
PTX and CUBIN inputs are rejected for this target because they carry no LTOIR
18+
and nvJitLink would otherwise silently omit them.
19+
(`#2369 <https://github.com/NVIDIA/cuda-python/issues/2369>`__)
20+
1221
- Added the read-only :attr:`ManagedBuffer.last_prefetch_location` property,
1322
which reports the destination requested by the most recent explicit
1423
prefetch across the buffer. It returns a :class:`Device` or :class:`Host`,

0 commit comments

Comments
 (0)