Repository navigation
Description
Is this a duplicate?
- I confirmed there appear to be no duplicate issues for this bug and that I agree to the Code of Conduct
Type of Bug
Runtime Error
Component
cuda.bindings
Describe the bug
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
Environment:
- Commit: d649075
- Linux aarch64, Python 3.15.2 free-threaded
- 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.
Operating System
No response
nvidia-smi output
No response
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.