Skip to content

fix(core): accept the documented str symbol_mapping in ObjectCode.get_kernel - #2585

Open
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:module-get-kernel-str-symbol-mapping
Open

fix(core): accept the documented str symbol_mapping in ObjectCode.get_kernel#2585
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:module-get-kernel-str-symbol-mapping

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Every ObjectCode.from_* constructor annotates and documents symbol_mapping: dict[str, str] | None"a dictionary specifying how the unmangled symbol names (as keys) should be mapped to the mangled names". get_kernel encodes the name only on the mapping miss path (_module.pyx:802-808):

        try:
            name = self._sym_map[name]
        except KeyError:
            if isinstance(name, str):
                name = name.encode()

        cdef KernelHandle h_kernel = create_kernel_handle(self._h_library, <const char*>name)

On a hit, name is rebound to the mapped value and handed straight to <const char*>. cuda_core sets no c_string_type / c_string_encoding directive — build_hooks.py:224 passes only embedsignature, warn.deprecated.IF and freethreading_compatible — so Cython's default applies and a str raises TypeError: expected bytes, str found.

The documented form therefore fails for exactly the names it exists to translate:

obj = ObjectCode.from_cubin(cubin, symbol_mapping={"saxpy<double>": mangled_str})
obj.get_kernel("saxpy<double>")   # TypeError: expected bytes, str found
obj.get_kernel("not_in_the_map")  # works

Why this has gone unnoticed: Program.compile fills the mapping from nvrtcGetLoweredName (_program.pyx:905-906), whose const char* Cython converts to bytes. Every existing test round-trips mod.symbol_mapping straight back into a from_* constructor (test_module.py:340, 354, 367, 380, 395, 412, 422, 435), so only bytes values are ever exercised. ObjectCode.symbol_mapping is likewise annotated dict[str, str] while returning bytes values.

Fix

Move the encode past the lookup so both value types work. Compile-produced mappings are byte-for-byte unaffected.

Tests

test_object_code_symbol_mapping_accepts_str_values, modelled on the adjacent test_object_code_load_cubin: decode the compile-produced mapping to dict[str, str], rebuild the ObjectCode from it, and get_kernel through a mapped name.

What I ran

Environment: macOS, no CUDA driver and no CUDA toolkit, so cuda.core cannot be built or imported here.

  • Did not run: the new test or the rest of test_module.py — they need a built cuda.core and a GPU.
  • Ran (teeth check): Cython is installed here, so I compiled the before/after name-handling verbatim into a standalone extension (language_level=3, no string directives — matching build_hooks.py) and drove it with both value types:
  hit, str value  (documented dict[str, str])      before -> TypeError: expected bytes, str found   after -> b'_Z5saxpyIdEvT_PKS0_S2_PS0_i'
  hit, bytes value (what Program.compile stores)   before -> b'_Z5saxpyIdEvT_PKS0_S2_PS0_i'         after -> b'_Z5saxpyIdEvT_PKS0_S2_PS0_i'
  miss, str name                                   before -> b'saxpy'                               after -> b'saxpy'
  miss, bytes name                                 before -> b'saxpy'                               after -> b'saxpy'

So the change fixes the documented case and leaves the other three byte-identical.

  • Ran: python -m py_compile, ruff check, ruff format --check on cuda_core/tests/test_module.py — clean, no new findings against a main baseline.
  • Checked: grep -rn "c_string_type\|c_string_encoding" cuda_core/ finds nothing, so Cython's default strchar* conversion (which rejects str) is what applies.
  • Not changed here: ObjectCode.symbol_mapping's -> dict[str, str] annotation still describes bytes values for compile-produced mappings. Correcting that annotation is a separate, wider question (it would need to become dict[str, str | bytes], or the compile path would need to decode), so I left it rather than widen this PR.

@copy-pr-bot

copy-pr-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the cuda.core Everything related to the cuda.core module label Aug 9, 2026

@mdboom mdboom left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this is a "pave over" fix rather than fixing the underlying issue.

The following findings from Claude illustrate the proper fix.

The fix patches the call site rather than the data model. _sym_map holds bytes values when populated by NVRTC and str values when user-supplied — a heterogeneous type that requires a runtime probe on every get_kernel call. The cleaner fix would normalize all values to bytes inside _init at storage time, so _sym_map is always dict[str, bytes] and the isinstance check disappears entirely.

The symbol_mapping property is annotated dict[str, str], but values from the NVRTC path are actually bytes (Cython auto-converts const char*), and the else None branch makes None also possible. The annotation should be dict[str, bytes | None]. This mismatch also flows into all six factory methods (from_ptx, from_cubin, etc.) which accept symbol_mapping: dict[str, str] | None.

Pre-existing dead-code risk — _program.pyx:902 (not introduced by PR)

symbol_mapping[n] = lowered_name if lowered_name != NULL else None — the None branch is practically unreachable because HANDLE_RETURN_NVRTC guarantees success before this line, but if it ever were reached, a subsequent get_kernel would get name = None after the map hit, isinstance(None, str) is False, and <const char*>None would segfault. We should plug this hole as well.

Comment thread cuda_core/cuda/core/_module.pyx Outdated
Comment on lines +806 to +812
# Encode after the lookup, not only on the miss path. symbol_mapping is
# annotated and documented `dict[str, str]`, but a str mapped value used
# to reach `<const char*>` unencoded and raise
# "TypeError: expected bytes, str found". Program.compile stores the
# lowered names as bytes (nvrtcGetLoweredName), so only the documented
# hand-built form was affected -- i.e. the mapping worked only when it
# did nothing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unnecessarily long comment.

Suggested change
# Encode after the lookup, not only on the miss path. symbol_mapping is
# annotated and documented `dict[str, str]`, but a str mapped value used
# to reach `<const char*>` unencoded and raise
# "TypeError: expected bytes, str found". Program.compile stores the
# lowered names as bytes (nvrtcGetLoweredName), so only the documented
# hand-built form was affected -- i.e. the mapping worked only when it
# did nothing.

Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted.
(`#2439 <https://github.com/NVIDIA/cuda-python/issues/2439>`__)

- :meth:`ObjectCode.get_kernel` now accepts a ``symbol_mapping`` whose values

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Put this under a new Bugfix header.

…_kernel

Every `ObjectCode.from_*` constructor annotates and documents
`symbol_mapping: dict[str, str] | None`. `get_kernel` encodes the name only
on the mapping MISS path:

    try:
        name = self._sym_map[name]
    except KeyError:
        if isinstance(name, str):
            name = name.encode()

    cdef KernelHandle h_kernel = create_kernel_handle(self._h_library, <const char*>name)

On a HIT, `name` is rebound to the mapped value and handed straight to
`<const char*>`. cuda_core sets no `c_string_type`/`c_string_encoding`
directive (build_hooks.py passes only embedsignature, warn.deprecated.IF and
freethreading_compatible), so Cython's default applies and a `str` raises
"TypeError: expected bytes, str found".

So the documented form fails for exactly the names it is supposed to
translate:

    ObjectCode.from_cubin(cubin, symbol_mapping={"saxpy<double>": mangled_str})
        .get_kernel("saxpy<double>")     # TypeError
        .get_kernel("not_in_the_map")    # fine

The reason this has gone unnoticed: `Program.compile` fills the mapping from
`nvrtcGetLoweredName`, whose `const char*` Cython converts to `bytes`, and
every existing test round-trips `mod.symbol_mapping` straight back into a
`from_*` constructor -- so only bytes values are ever exercised.

Move the encode past the lookup so both value types work. Compile-produced
mappings are unaffected.
@LeSingh1
LeSingh1 force-pushed the module-get-kernel-str-symbol-mapping branch from 7a1ff27 to 7400230 Compare August 11, 2026 19:55
@LeSingh1

Copy link
Copy Markdown
Contributor Author

You were right that this paved over the problem. Reworked in 7400230.

_sym_map is now normalised to bytes in _init, so it is homogeneous and the isinstance probe at the get_kernel call site is gone. The remaining encode there is only for a caller-supplied name that missed the map. The factory signatures now say dict[str, str | bytes] | None and the symbol_mapping property returns dict[str, bytes].

I also plugged the _program.pyx hole: a NULL lowered name now raises instead of storing None, since None would reach <const char*> and segfault.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cuda.core Everything related to the cuda.core module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants