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
26 changes: 21 additions & 5 deletions cuda_core/cuda/core/_linker.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ class LinkerOptions:

def __post_init__(self) -> None:
_lazy_init()
# `name` is annotated `str | None`, so None must fall back to the
# documented default instead of raising AttributeError from
# `.encode()`. Mirrors the same fix for ProgramOptions (#2517).
if self.name is None:
self.name = "<default linker>"
self._name = self.name.encode()

def _prepare_nvjitlink_options(self, as_bytes: bool = False) -> list[bytes] | list[str]:
Expand All @@ -321,7 +326,11 @@ class LinkerOptions:
options.append("-arch=sm_" + "".join(f"{i}" for i in Device().compute_capability))
if self.max_register_count is not None:
options.append(f"-maxrregcount={self.max_register_count}")
if self.time is not None:
# `-time` is a valueless switch, so it must be gated on truthiness
# like every other valueless flag here (verbose, -lto, -ptx, -g,
# -lineinfo, -no-cache). `is not None` is only right for the flags
# that emit an explicit value, e.g. `-ftz=true|false` below.
if self.time:
options.append("-time")
if self.verbose:
options.append("-verbose")
Expand All @@ -343,19 +352,24 @@ class LinkerOptions:
options.append(f"-prec-sqrt={'true' if self.prec_sqrt else 'false'}")
if self.fma is not None:
options.append(f"-fma={'true' if self.fma else 'false'}")
# Accept any sequence, not just `list`: both fields are annotated (and
# documented) `str | tuple[str] | list[str]`, and a tuple used to match
# neither branch, so it emitted no option and raised nothing.
# `ptxas_options` below already gets this right.
if self.kernels_used is not None:
if isinstance(self.kernels_used, str):
options.append(f"-kernels-used={self.kernels_used}")
elif isinstance(self.kernels_used, list):
elif is_sequence(self.kernels_used):
for kernel in self.kernels_used:
options.append(f"-kernels-used={kernel}")
if self.variables_used is not None:
if isinstance(self.variables_used, str):
options.append(f"-variables-used={self.variables_used}")
elif isinstance(self.variables_used, list):
elif is_sequence(self.variables_used):
for variable in self.variables_used:
options.append(f"-variables-used={variable}")
if self.optimize_unused_variables is not None:
# Valueless switch: see the `-time` note above.
if self.optimize_unused_variables:
options.append("-optimize-unused-variables")
if self.ptxas_options is not None:
if isinstance(self.ptxas_options, str):
Expand Down Expand Up @@ -398,7 +412,9 @@ class LinkerOptions:
if self.max_register_count is not None:
formatted_options.append(self.max_register_count)
option_keys.append(_driver.CUjit_option.CU_JIT_MAX_REGISTERS)
if self.time is not None:
# Only an option the caller actually turned ON is unsupported;
# `time=False` asks for nothing and must not be rejected.
if self.time:
raise ValueError("time option is not supported by the driver API")
if self.verbose:
formatted_options.append(1)
Expand Down
10 changes: 10 additions & 0 deletions cuda_core/docs/source/release/1.2.0-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ Fixes and enhancements
Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted.
(`#2439 <https://github.com/NVIDIA/cuda-python/issues/2439>`__)

- Four :class:`LinkerOptions` fields now behave as documented.
``time=False`` and ``optimize_unused_variables=False`` no longer *enable*
those flags -- both are valueless nvJitLink switches that were gated on
``is not None``, so explicitly disabling them turned them on (and, on the
driver backend, ``time=False`` raised "not supported"). ``kernels_used`` and
``variables_used`` now accept the documented ``tuple`` form, which
previously matched no branch and emitted nothing at all. ``name=None`` now
falls back to the documented default instead of raising ``AttributeError``,
matching :class:`ProgramOptions`.

Deprecation Notices
-------------------

Expand Down
61 changes: 61 additions & 0 deletions cuda_core/tests/test_linker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# SPDX-License-Identifier: Apache-2.0

import inspect
import warnings

import pytest

Expand Down Expand Up @@ -436,6 +437,66 @@ def test_prepare_driver_options_unsupported_raises(driver_binding, kwargs, match
opts._prepare_driver_options()


@pytest.mark.agent_authored(model="claude-opus-5")
@pytest.mark.parametrize(
("field", "flag"),
[("time", "-time"), ("optimize_unused_variables", "-optimize-unused-variables")],
)
def test_valueless_flags_are_gated_on_truthiness(field, flag):
"""A valueless nvJitLink switch must not be emitted for ``False``.

Both were gated on ``is not None``, which is only correct for the flags
that emit an explicit value (``-ftz=true|false`` and friends). So
``time=False`` turned timing *on*, and ``optimize_unused_variables=False``
turned the optimization *on* -- silently changing the linked binary by
dropping device variables the caller asked to keep.
"""
assert flag in LinkerOptions(arch=ARCH, **{field: True})._prepare_nvjitlink_options()
assert flag not in LinkerOptions(arch=ARCH, **{field: False})._prepare_nvjitlink_options()
assert flag not in LinkerOptions(arch=ARCH)._prepare_nvjitlink_options()


@pytest.mark.agent_authored(model="claude-opus-5")
@pytest.mark.parametrize(
("field", "flag"),
[("kernels_used", "-kernels-used"), ("variables_used", "-variables-used")],
)
@pytest.mark.parametrize("factory", [list, tuple], ids=["list", "tuple"])
def test_sequence_options_accept_tuples(field, flag, factory):
"""``str | tuple[str] | list[str]`` must all reach the linker.

The dispatch tested ``isinstance(..., list)``, so the documented tuple form
matched neither branch: no option was emitted and nothing was raised, and
the link silently kept every kernel/variable the caller meant to filter.
"""
emitted = LinkerOptions(arch=ARCH, **{field: factory(("A", "B"))})._prepare_nvjitlink_options()
assert f"{flag}=A" in emitted
assert f"{flag}=B" in emitted


@pytest.mark.agent_authored(model="claude-opus-5")
def test_linker_options_accepts_name_none():
"""``name`` is annotated ``str | None``; ``None`` must fall back to the
documented default instead of raising ``AttributeError`` from ``.encode()``.
Same fix as ProgramOptions received in #2517."""
assert LinkerOptions(arch=ARCH, name=None).name == "<default linker>"


@pytest.mark.agent_authored(model="claude-opus-5")
@pytest.mark.parametrize("field", ["time", "optimize_unused_variables"])
def test_prepare_driver_options_ignores_disabled_flags(driver_binding, field):
"""An option the caller turned OFF is not an option the driver must support.

``time=False`` raised "time option is not supported by the driver API" and
``optimize_unused_variables=False`` emitted a DeprecationWarning, both for
a flag that would never have been sent.
"""
opts = LinkerOptions(arch="sm_80", **{field: False})
with warnings.catch_warnings():
warnings.simplefilter("error")
opts._prepare_driver_options()


def test_linker_empty_object_codes_raises():
"""Linker with no ObjectCode raises ValueError."""
with pytest.raises(ValueError, match="At least one ObjectCode object must be provided"):
Expand Down
Loading