fix(core): accept the documented str symbol_mapping in ObjectCode.get_kernel - #2585
fix(core): accept the documented str symbol_mapping in ObjectCode.get_kernel#2585LeSingh1 wants to merge 1 commit into
Conversation
mdboom
left a comment
There was a problem hiding this comment.
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.
| # 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. |
There was a problem hiding this comment.
Unnecessarily long comment.
| # 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 |
There was a problem hiding this comment.
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.
7a1ff27 to
7400230
Compare
|
You were right that this paved over the problem. Reworked in 7400230.
I also plugged the |
Problem
Every
ObjectCode.from_*constructor annotates and documentssymbol_mapping: dict[str, str] | None— "a dictionary specifying how the unmangled symbol names (as keys) should be mapped to the mangled names".get_kernelencodes the name only on the mapping miss path (_module.pyx:802-808):On a hit,
nameis rebound to the mapped value and handed straight to<const char*>.cuda_coresets noc_string_type/c_string_encodingdirective —build_hooks.py:224passes onlyembedsignature,warn.deprecated.IFandfreethreading_compatible— so Cython's default applies and astrraisesTypeError: expected bytes, str found.The documented form therefore fails for exactly the names it exists to translate:
Why this has gone unnoticed:
Program.compilefills the mapping fromnvrtcGetLoweredName(_program.pyx:905-906), whoseconst char*Cython converts tobytes. Every existing test round-tripsmod.symbol_mappingstraight back into afrom_*constructor (test_module.py:340, 354, 367, 380, 395, 412, 422, 435), so onlybytesvalues are ever exercised.ObjectCode.symbol_mappingis likewise annotateddict[str, str]while returningbytesvalues.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 adjacenttest_object_code_load_cubin: decode the compile-produced mapping todict[str, str], rebuild theObjectCodefrom it, andget_kernelthrough a mapped name.What I ran
Environment: macOS, no CUDA driver and no CUDA toolkit, so
cuda.corecannot be built or imported here.test_module.py— they need a builtcuda.coreand a GPU.language_level=3, no string directives — matchingbuild_hooks.py) and drove it with both value types:So the change fixes the documented case and leaves the other three byte-identical.
python -m py_compile,ruff check,ruff format --checkoncuda_core/tests/test_module.py— clean, no new findings against amainbaseline.grep -rn "c_string_type\|c_string_encoding" cuda_core/finds nothing, so Cython's defaultstr→char*conversion (which rejectsstr) is what applies.ObjectCode.symbol_mapping's-> dict[str, str]annotation still describesbytesvalues for compile-produced mappings. Correcting that annotation is a separate, wider question (it would need to becomedict[str, str | bytes], or the compile path would need to decode), so I left it rather than widen this PR.