You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A nightly CI job aborted with exit code 134 while running tests/legacy_api/test_legacy_cuda.py::test_culogs_functions with four parallel threads. Failing job
CUDA Toolkit 13.4.2, NVIDIA driver 595.99.02, L4 GPU
pytest-run-parallel==0.10.0, --parallel-threads=4, --count=5
The Python stack points to cuLogsUnregisterCallback(callback_handle). Disassembly of the exact wheel places the native abort in map-node deletion during std::map::erase().
The legacy bindings store callback allocations in the shared m_global._allocated C++ map.
cuLogsRegisterCallback inserts entries, while cuLogsUnregisterCallback frees callback data and erases entries without synchronization. Concurrent access under free-threaded Python is the likely cause of memory corruption. Async-notification callback APIs also access this map.
Expected behavior: concurrent registration and unregistration of independent callbacks completes safely.
Suggested fix: synchronize callback bookkeeping at the binding-generation source, covering all APIs that share the map, and add concurrent regression coverage. Serializing the affected test could provide an interim CI mitigation.
The full CUDA test has not been reproduced locally. The matching ARM Python 3.14t job passed on the same driver version, suggesting an intermittent race.
Some history:
Recent test changes likely exposed an older bug. I found no recent change introducing the unsafe map operations.
May 2025: The cuLogs* wrappers already used the shared map without synchronization. (#597)
September 2, 2026: CI enabled four parallel test threads for free-threaded Python. (#2194)
October 5: test_culogs_functions was added, exercising registration and cleanup. (#2978)
October 15: Context pooling increased actual overlap between worker threads. The commit explicitly notes that context creation previously kept them apart. This is the strongest candidate for making the existing race easier to trigger. (#3122)
How to Reproduce
Not reproducible.
Expected behavior
Expected behavior: concurrent registration and unregistration of independent callbacks completes safely.
Another instance of the same thread-unsafety class turned up, this time on Windows and in a different (non-legacy) binding module, so I'm recording it here rather than opening a near-duplicate.
cuda_bindings_13/tests/test_cuda.py::test_private_function_pointer_inspector failed under pytest-run-parallel (the test ran in PARALLEL mode):
PARALLEL FAILED tests/test_cuda.py::test_private_function_pointer_inspector - SystemError: ...\Python-3.14.10\Objects\dictobject.c:2468: bad argument to internal function
The test is a one-liner that just calls into the private inspector:
The failure is unrelated to the PR whose CI surfaced it; it is a pre-existing latent race, consistent with the "recent parallelism changes expose an older bug" history in the issue description.
Diagnosis
Same root pattern as the callback map, but in generated host-API bindings: a module-level mutable global mutated with an unsynchronized check-then-set. In cuda/bindings/_internal/driver_windows.pyx (the same shape exists in driver_linux.pyx and in every other _internal/*.pyx module: runtime, nvrtc, nvjitlink, nvvm, cufile, ...):
cdef dict _cyb_func_ptrs =None# module-level global
cpdef dict _inspect_function_pointers():
global _cyb_func_ptrs
if _cyb_func_ptrs isnotNone:
return _cyb_func_ptrs
_check_or_init_driver()
cdef dict data = {}
# ... populate data["__cu..."] = <intptr_t>__cu... for every entry point ...
_cyb_func_ptrs = data
return data
cpdef _inspect_function_pointer(str name):
global _cyb_func_ptrs
if _cyb_func_ptrs isNone:
_cyb_func_ptrs = _inspect_function_pointers()
return _cyb_func_ptrs[name]
Under free-threaded Python the lazy init races: multiple threads can simultaneously observe _cyb_func_ptrs is None, each build a separate data dict, and assign/read the global concurrently. _cyb_func_ptrs[name] is a cdef dict subscript that Cython lowers to the internal CPython dict C-API; when that API is entered against a dict object being mutated/replaced on another thread without the GIL, it trips the dictobject.c precondition assertion and raises SystemError: bad argument to internal function. The GIL previously made this check-then-set effectively atomic, which is why it only shows up on the free-threaded build.
So this is the same bug class as the m_global._allocated map in this issue — shared binding state mutated without synchronization — just manifesting as a corrupted/racily-replaced lazy singleton dict in the _inspect_function_pointer* path rather than a std::map::erase abort.
Suggested fix
Fix it at the binding-generation source, consistent with the fix proposed here for the callback map:
Make the lazy init of _cyb_func_ptrs atomic. The simplest robust option is to populate it eagerly at module import (the data is just a static table of entry-point addresses after _check_or_init_driver()), which removes the racy singleton entirely. If lazy init must be retained, guard the check-then-set with a module-level lock (e.g. a threading.Lock), or build into a local and publish with a single atomic assignment plus a re-check, so readers only ever see a fully-populated dict.
Apply the change in the generator so it covers every _internal/*.pyx module and both _linux/_windows variants, since they all share this pattern.
Add concurrent regression coverage that calls _inspect_function_pointer from multiple threads.
Interim CI mitigation (symptom-only, mirrors the "serialize the affected test" suggestion above): mark test_private_function_pointer_inspector with @pytest.mark.thread_unsafe so it is not scheduled in parallel. This unblocks CI but leaves the underlying API thread-unsafe for real callers (e.g. cuda.core._linker calls nvjitlink._inspect_function_pointer("__nvJitLinkVersion")).
Thanks for the detailed report. This looks like two separate problems, but I suspect they share a root cause: both involve a global variable being used by multiple threads at the same time without protection.
I've got a chance to look at the first one closely and can now reproduce it (Thanks for the detailed info @jpascucci-nv !). The suspected cause is the global m_global._allocated map in the generated driver bindings. cuLogsRegisterCallback inserts into it and cuLogsUnregisterCallback frees and erases from it, with no locking. With the GIL disabled, concurrent threads modify the same std::map, which corrupts the heap.
Next, I'll add a lock around the map accesses locally to confirm that this makes the crash go away. If it does, I'd like to check with the author who introduced this variable whether a lock-based fix is acceptable.
Will look into the problem @juenglin mentioned to see whether it has the same cause.
The Windows failure in test_private_function_pointer_inspector reported above (by @juenglin) is a separate problem (an unsynchronized lazy init, not the cuLogs callback map), so I split it out into #3373.
This issue now only tracks the cuLogs crash fixed in #3308, with the backport to 12.9 remaining.
Activity
juenglin commentedon Oct 29, 2026
Another instance of the same thread-unsafety class turned up, this time on Windows and in a different (non-legacy) binding module, so I'm recording it here rather than opening a near-duplicate.
Observation
Failing job: Test win-64 / Python 3.14t, CUDA 13.4.2 (wheels), GPU a100 (MCDM)
cuda_bindings_13/tests/test_cuda.py::test_private_function_pointer_inspectorfailed underpytest-run-parallel(the test ran inPARALLELmode):The test is a one-liner that just calls into the private inspector:
The failure is unrelated to the PR whose CI surfaced it; it is a pre-existing latent race, consistent with the "recent parallelism changes expose an older bug" history in the issue description.
Diagnosis
Same root pattern as the callback map, but in generated host-API bindings: a module-level mutable global mutated with an unsynchronized check-then-set. In
cuda/bindings/_internal/driver_windows.pyx(the same shape exists indriver_linux.pyxand in every other_internal/*.pyxmodule:runtime,nvrtc,nvjitlink,nvvm,cufile, ...):Under free-threaded Python the lazy init races: multiple threads can simultaneously observe
_cyb_func_ptrs is None, each build a separatedatadict, and assign/read the global concurrently._cyb_func_ptrs[name]is acdef dictsubscript that Cython lowers to the internal CPython dict C-API; when that API is entered against a dict object being mutated/replaced on another thread without the GIL, it trips thedictobject.cprecondition assertion and raisesSystemError: bad argument to internal function. The GIL previously made this check-then-set effectively atomic, which is why it only shows up on the free-threaded build.So this is the same bug class as the
m_global._allocatedmap in this issue — shared binding state mutated without synchronization — just manifesting as a corrupted/racily-replaced lazy singleton dict in the_inspect_function_pointer*path rather than astd::map::eraseabort.Suggested fix
Fix it at the binding-generation source, consistent with the fix proposed here for the callback map:
_cyb_func_ptrsatomic. The simplest robust option is to populate it eagerly at module import (the data is just a static table of entry-point addresses after_check_or_init_driver()), which removes the racy singleton entirely. If lazy init must be retained, guard the check-then-set with a module-level lock (e.g. athreading.Lock), or build into a local and publish with a single atomic assignment plus a re-check, so readers only ever see a fully-populated dict._internal/*.pyxmodule and both_linux/_windowsvariants, since they all share this pattern._inspect_function_pointerfrom multiple threads.Interim CI mitigation (symptom-only, mirrors the "serialize the affected test" suggestion above): mark
test_private_function_pointer_inspectorwith@pytest.mark.thread_unsafeso it is not scheduled in parallel. This unblocks CI but leaves the underlying API thread-unsafe for real callers (e.g.cuda.core._linkercallsnvjitlink._inspect_function_pointer("__nvJitLinkVersion")).lijinf2 commentedon Oct 30, 2026
Thanks for the detailed report. This looks like two separate problems, but I suspect they share a root cause: both involve a global variable being used by multiple threads at the same time without protection.
I've got a chance to look at the first one closely and can now reproduce it (Thanks for the detailed info @jpascucci-nv !). The suspected cause is the global
m_global._allocatedmap in the generated driver bindings.cuLogsRegisterCallbackinserts into it andcuLogsUnregisterCallbackfrees and erases from it, with no locking. With the GIL disabled, concurrent threads modify the samestd::map, which corrupts the heap.Next, I'll add a lock around the map accesses locally to confirm that this makes the crash go away. If it does, I'd like to check with the author who introduced this variable whether a lock-based fix is acceptable.
Will look into the problem @juenglin mentioned to see whether it has the same cause.
Fix crash when registering/unregistering callbacks concurrently under…
lijinf2 commentedon Nov 3, 2026
Need to backport to 12.9 and investigate
test_private_function_pointer_inspectorlijinf2 commentedon Nov 4, 2026
The Windows failure in
test_private_function_pointer_inspectorreported above (by @juenglin) is a separate problem (an unsynchronized lazy init, not the cuLogs callback map), so I split it out into #3373.This issue now only tracks the cuLogs crash fixed in #3308, with the backport to 12.9 remaining.