Repository navigation
Don't touch destroyed contexts when releasing their resources - #3404
Merged
Merged
Conversation
Contributor
CUDA.jl BenchmarksDetails
This comment was automatically generated by workflow using github-action-benchmark. |
maleadt
force-pushed
the
tb/destroyed-context-release
branch
2 times, most recently
from
October 19, 2026 07:42
a6ee5eb to
d4a2891
Compare
Since finalizers only retire resources (#3318), a resource can be released after its context has been destroyed, e.g., when leaving a `CuContext(dev) do ... end` block. The release then activated the dead context, which crashes inside the driver. Destroying a context already released its resources, and their handles may since have been reused, so drop them instead, as CUDA.jl did before `skip_destroyed` was removed. Host-side bookkeeping (memory accounting, leases, pin counts) is still updated. Some resources outlive the context they were created in: graphs, executable graphs, and allocations from a memory pool. Those are released in another, live context. Allocations made by graphs outlive it too, but the device isn't known when freeing them, so those still leak. Fixes #3403
A task's stream is cached per device, but belongs to the context it was created in. After switching to another context on the same device, e.g., one created with `CuContext(dev)`, the task kept using the stream of the previous context there, and when the stream was first created in the other context, the task kept it afterwards, failing once that context was destroyed. Forget the cached stream when switching contexts. A do-block switch restores the task's stream afterwards; otherwise, the task claims its pooled stream of that context again.
When a task's first use of CUDA was a `context!(ctx) do ... end` or `device!(dev) do ... end` block, there was no previous context to switch back to, so the task stayed in `ctx` afterwards. With a context created by `CuContext(dev)`, that context is typically destroyed right after, and the task's next CUDA call crashed. Forget the task's state at the end of such a block, so that it is initialized again on next use.
Every synchronization checks the exception flag of each context a kernel was loaded in. That flag lives in host memory that is freed when the context is destroyed, so reading it afterwards crashed. Skip those contexts again, as before #3178.
maleadt
force-pushed
the
tb/destroyed-context-release
branch
from
October 19, 2026 20:19
d4a2891 to
1a207bb
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3404 +/- ##
==========================================
- Coverage 87.08% 85.14% -1.94%
==========================================
Files 192 191 -1
Lines 19543 19307 -236
==========================================
- Hits 17019 16439 -580
- Misses 2524 2868 +344 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Since #3318, finalizers only retire resources, and a regular task releases them later. If the resource's context is destroyed in between, e.g. by leaving a
CuContext(dev) do ... endblock, the release activates the dead context, and the driver segfaults (cuCtxGetDeviceright aftercuCtxSetCurrenton the dangling handle). cuTile.jl's test suite hits this. On main, streams, events, graphs, pinned memory, and arrays or kernels used on a stream of a temporary context all crash or hang this way:Destroying a context releases what it owns, and the handles may be reused afterwards, so release paths now check
isvalid(ctx)before doing context-dependent driver work, and drop the resource if the context is gone. Before #3178 removed it,skip_destroyeddid the same thing. Only the driver calls are skipped: host-side bookkeeping (memory accounting, graph leases, pin counts) still runs. If a held resource's gate event belongs to a dead context, the resource counts as done, anddisposal_streamsforgets the streams of destroyed contexts.Some resources outlive their context, so dropping them would leak them:
cuCtxDestroy, and thatcuMemFreeAsyncfrom the primary context frees it. They are now freed in a live context: the allocating context if it still exists, otherwise the primary context of the device.GraphAllocationsdoesn't record its device. There's anXXXcomment for it.Memory can also be last used in a context other than the one it was allocated in. If that context is destroyed first, reusing cached host or unified memory (without a memory pool) no longer queries its idle event, which segfaults after destruction, and CPU access to globally attached unified memory attaches it in the context the memory lives in.
A second commit fixes a related crash that has existed since #3178: every synchronization reads the exception flag of every context a kernel was loaded in, and for a destroyed context that host memory has already been freed.
check_exceptionsskips such contexts again.The validity check is one
cuCtxGetIdcall, about 21 ns, per released resource. Other per-context state stays stale after a context is destroyed (stream pools, library handle caches, the kernel cache). Using kernels from a non-primary context withcontext!is also still broken (CUDA Stream must be associated with the same CUDA Context that loaded CUFunction), on 6.4.2 as well; that's a separate issue.Tested on an RTX 5080: the new tests in
core/resourcesandcore/exceptionscrash their worker on main and pass with this PR. A set of reproducers (stream, event, graph, pinned memory, an array and a kernel launch on the context's own stream, a stream that was released before the context was destroyed, cached host memory last used in a destroyed context) crash, abort or hang on main and now run without errors. Pool memory allocated in the temporary context is freed.Fixes #3403