Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions cuda_core/cuda/core/_linker.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,22 @@ cdef class Linker:
else:
return as_py(self._culink_handle)

@property
def backend(self) -> str:
"""Return this Linker instance's underlying backend."""
return "nvJitLink" if self._use_nvjitlink else "driver"
@classmethod
def backend(cls) -> str:
"""Return which linking backend will be used.

Returns ``"nvJitLink"`` when the nvJitLink library is available
and meets the minimum version requirement, otherwise ``"driver"``.

.. note::

Prefer letting :class:`Linker` decide. Query ``backend()``
only when you need to dispatch based on input format (for
example: choose PTX vs. LTOIR before constructing a
``Linker``). The returned string names an implementation
detail whose support matrix may shift across CTK releases.
"""
return "driver" if _decide_nvjitlink_or_driver() else "nvJitLink"
Comment on lines -170 to +185
Copy link
Copy Markdown
Contributor

@Andy-Jost Andy-Jost May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We did just complete a sweep to standardize the property/method distinction. It would be great to keep that as clean as possible.

One option is to roll our own classproperty:

class classproperty:
    def __init__(self, func):
        self.func = func
    def __get__(self, instance, owner):
        return self.func(owner)

class Linker:
    @classproperty
    def backend(cls):
        . . .

Related: python/cpython#89519

There are caveats and hazards in complex cases, but they wouldn't come into play here.

Another option would be to keep the property while adding a module-level function get_backend().

Is there any risk that the backend selection might become dependent on constructor args? If so, it would be better to use a freestanding function IMO.



# =============================================================================
Expand Down
2 changes: 1 addition & 1 deletion cuda_core/cuda/core/_program.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ cdef inline int Program_init(Program self, object code, str code_type, object op
self._linker = Linker(
ObjectCode._init(code.encode(), code_type), options=_translate_program_options(options)
)
self._backend = self._linker.backend
self._backend = self._linker.backend()

elif code_type == "nvvm":
_get_nvvm_module() # Validate NVVM availability
Expand Down
18 changes: 12 additions & 6 deletions cuda_core/docs/source/release/1.0.0-notes.rst
Comment thread
leofang marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ Highlights
- TBD


New features
------------

- TBD


Breaking changes
----------------

Expand Down Expand Up @@ -113,6 +107,18 @@ Breaking changes
``CUgraphConditionalHandle`` value. Previously, ``.handle`` had to be
extracted explicitly.

- :meth:`Linker.backend` is now a classmethod instead of an instance property.
Call sites must use ``Linker.backend()`` (with parentheses) instead of
``linker.backend``. This allows querying the linking backend without
constructing a ``Linker`` instance — for example, to choose between PTX and
LTOIR input before linking.
Comment thread
leofang marked this conversation as resolved.


New features
------------

- TBD


Fixes and enhancements
-----------------------
Expand Down
41 changes: 40 additions & 1 deletion cuda_core/tests/test_linker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#
# SPDX-License-Identifier: Apache-2.0

import inspect

import pytest

from cuda.core import Device, Linker, LinkerOptions, Program, ProgramOptions, _linker
Expand Down Expand Up @@ -92,7 +94,7 @@ def test_linker_init(compile_ptx_functions, options):
linker = Linker(*compile_ptx_functions, options=options)
object_code = linker.link("cubin")
assert isinstance(object_code, ObjectCode)
assert linker.backend == ("driver" if is_culink_backend else "nvJitLink")
assert linker.backend() == ("driver" if is_culink_backend else "nvJitLink")


def test_linker_init_invalid_arch(compile_ptx_functions):
Expand Down Expand Up @@ -242,3 +244,40 @@ def test_linker_options_nvjitlink_options_as_str():
assert f"-arch={ARCH}" in options
assert "-g" in options
assert "-lineinfo" in options


class TestBackendClassmethod:
def test_backend_returns_nvjitlink(self, monkeypatch):
monkeypatch.setattr(_linker, "_use_nvjitlink_backend", True)
assert Linker.backend() == "nvJitLink"

def test_backend_returns_driver(self, monkeypatch):
monkeypatch.setattr(_linker, "_use_nvjitlink_backend", False)
assert Linker.backend() == "driver"

def test_backend_invokes_probe_when_not_memoised(self, monkeypatch):
monkeypatch.setattr(_linker, "_use_nvjitlink_backend", None)
called = []

def fake_decide():
called.append(True)
return False # False = not falling back to driver = nvJitLink

monkeypatch.setattr(_linker, "_decide_nvjitlink_or_driver", fake_decide)
result = Linker.backend()
assert result == "nvJitLink"
assert called, "_decide_nvjitlink_or_driver was not called"

def test_backend_is_classmethod(self):
attr = inspect.getattr_static(Linker, "backend")
assert isinstance(attr, classmethod)

def test_backend_is_not_property(self):
"""backend is a classmethod, not a property.

This is an intentional breaking change from the prior property API.
Attribute-style access (``linker.backend``) now returns a bound method,
not a string. All call sites must use parens: ``Linker.backend()``.
"""
attr = inspect.getattr_static(Linker, "backend")
assert not isinstance(attr, property)
Loading