Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
|
/ok to test |
|
5b5b623 to
c3a72aa
Compare
|
/ok to test |
1 similar comment
|
/ok to test |
af42fb1 to
7ce9893
Compare
DeviceMemoryResource(device) with no options raises the release threshold of the driver's pool with cuMemPoolGetAttribute and cuMemPoolSetAttribute. The driver refuses both as potentially unsafe calls while the calling thread is inside a global or thread-local capture, and it invalidates the capture. Device.memory_resource constructs the resource lazily, so a first allocation could invalidate a capture in progress. Make the two calls in relaxed capture mode and restore the thread's previous mode afterwards. A failure to restore the mode is attached to the propagating error as a note. Ending an invalidated capture made the builder destroy a graph the driver had already destroyed. cuStreamEndCapture returns a NULL graph for an invalidated (or unjoined) capture and releases the capture graph itself, but the builder kept the owning handle it took from cuStreamGetCaptureInfo and its deleter called cuGraphDestroy again: a use-after-free that segfaulted at close() or garbage collection. Add invalidate_root_graph_state to retire the hierarchy when the driver discards the root graph, and route end_building(), close() and __dealloc__ through one GB_end_capture helper that settles graph ownership from the end-capture result. end_building() now ends an invalidated capture and raises the driver error; the builder then holds no graph (new CAPTURE_INVALIDATED state), and complete(), debug_dot_print(), graph_definition, embed() and Graph.update() say so. close() closes the builder before raising. end_building() on a forked builder is rejected with RuntimeError instead of invalidating the capture. Fixes NVIDIA#2834. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
7ce9893 to
d64ba7e
Compare
The driver also discards the body graph of a conditional node when the body capture ends invalidated, and the parent graph keeps referring to it, which cuda.core cannot repair. Say so in end_building(), the release note, and the GB_end_capture comment. Scope the DeviceMemoryResource note to the default-pool constructor, since cuMemPoolCreate is still refused under capture. Drop the end_building() call on a forked builder from the skip path of test_graph_conditional_on_forked_builder. Issue NVIDIA#2834 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
join() waits on each forked builder's stream and then closes it. When a wait raised partway through, the forks the loop had not reached stayed open with capturing streams, and destroying them later during garbage collection crashed the interpreter. Close every fork the loop did not reach before the error propagates. The capture cannot complete without the work captured on those forks, so end_building() then raises the driver's unjoined-capture error and the builder closes cleanly, which NVIDIA#2834 made possible. Issue NVIDIA#2776 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
PR NVIDIA#2881 is folded into this PR: its join() cleanup relies on the invalidated-capture handling here, and the NVIDIA#2776 crash is the same double destroy of a graph the driver already discarded. Merge the two release-note entries and correct the test docstring, which attributed the crash to the fork rather than to the builder's teardown. Issue NVIDIA#2834, NVIDIA#2776 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An invalidated capture of a conditional body leaves the parent graph invalid, and ending the parent capture afterwards crashes inside the driver (NVIDIA#2918, a CUDA driver bug reproduced with the driver API alone). Say so in the end_building docstring and the release note instead of explaining the mechanism. Issue NVIDIA#2834 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| gb = Device().create_graph_builder().begin_building() | ||
| gb, bad, left, right = gb.split(4) | ||
| launch(left, LaunchConfig(grid=1, block=1), empty_kernel) | ||
| bad.stream.close() |
There was a problem hiding this comment.
Hmm I think we have to pass a primary whose end capture is actually refused to fully exercize things here. Provoking the RuntimeError from something we created via split makes it so that GB_end_capture simply short circuits.
import gc
from cuda.core import Device
from cuda.core.graph import GraphBuilder
dev = Device()
dev.set_current()
def bad_primary():
"""A primary whose close() fails: it has an unjoined fork, so the driver
refuses cuStreamEndCapture."""
p = dev.create_graph_builder().begin_building()
p, _unjoined = p.split(2)
return p
root = dev.create_graph_builder().begin_building()
root, fork = root.split(2)
bad1, bad2 = bad_primary(), bad_primary()
try:
GraphBuilder.join(root, bad1, bad2, fork)
except Exception as e:
print(f"join raised: {type(e).__name__}: {str(e).split(':')[0]}")
# join() documents that on failure "the builders that were not joined are
# closed before the error propagates, so none is left capturing".
left_open = [n for n, b in (("bad1", bad1), ("bad2", bad2), ("fork", fork)) if not b.is_closed]
print(f"left open: {left_open}")
del bad1, bad2, fork
gc.collect()this gives me, on this branch,
join raised: CUDAError: CUDA_ERROR_STREAM_CAPTURE_INVALIDATED
left open: ['bad2', 'fork']
So the first truly failing close stranded the ones later on in the close list.
There was a problem hiding this comment.
Thanks for catching that. I will upload a fix.
| // CUDA destroyed the root graph and, with it, every child. Retire every box | ||
| // so that no registry entry resolves to the dead graphs and the hierarchy's | ||
| // deleter finds no root to destroy. | ||
| void invalidate_root_graph_state(const GraphHandle& h_root) noexcept { |
There was a problem hiding this comment.
This looks like parts of it can be factored out from here and invalidate_child_graph_state below, would be a bit more DRY
There was a problem hiding this comment.
retire_graph(hierarchy, graph) ?
| error, after which the builder holds no graph. A failed ``join`` also closes | ||
| the forked builders it did not join. This does not extend to the body | ||
| builder of a conditional node: an invalidated body capture leaves the parent | ||
| graph invalid as well, and ending it may crash the process |
There was a problem hiding this comment.
This doesn't cut across
right?There was a problem hiding this comment.
The crash is a SEGV due to a driver bug with no known workaround. Issue #2918 has more details.
brandon-b-miller
left a comment
There was a problem hiding this comment.
Couple Q's otherwise LGTM.
join() closes the builders it did not join before its error propagates. That sweep ran in a finally block and called close() on each builder. A builder from another capture makes the root's wait fail, and the driver invalidates that builder's capture as well, so its close() raised too. The raise stopped the sweep, stranded the later builders with capturing streams, and replaced the merge error with the close error. Run the sweep in an except block, close each builder through GB_close, which returns the driver status instead of raising, and attach a failed close to the propagating error as a note (error handling policy). The original error is re-raised unchanged. Review feedback on NVIDIA#2838. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…anup The existing test provokes the join failure with a forked builder, whose close() cannot raise, so the sweep's own failure path went untested. Join two separate primaries into a capture: the driver refuses the cross-capture wait and invalidates the other capture, so closing that builder fails. The test checks that every builder is closed, that the merge error propagates, and that the failed close is attached as a note (or reported as a CUDAWarning on Python 3.10). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
invalidate_child_graph_state and invalidate_root_graph_state repeated the per-box retirement: detach node handles, drop the registry entry and attachments, move the box to the graveyard. Both now call retire_graph and differ only in which boxes they select. No behavior change. Review feedback on NVIDIA#2838. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
64b5925 includes fixes for the things @brandon-b-miller pointed out |
Summary
Fixes #2834, both parts, and #2776.
DeviceMemoryResource(device)can be constructed for the device's default pool, andDevice.memory_resourcefirst touched, while the calling thread is inside aglobalorthread_localstream capture, without failing or invalidating the capture. Ending, closing, or collecting a top-levelGraphBuilderwhose capture was invalidated, or left unjoined by ajoin()that failed partway, no longer segfaults.Both minimal reproducers from #2834 were run against this branch on a local GPU host: every scenario of the first now leaves the capture active, and the second survives 200 invalidate-and-collect rounds in each capture mode.
Changes
_memory_pool.pyx:MP_raise_release_thresholdmakes itscuMemPoolGetAttribute/cuMemPoolSetAttributecalls in relaxed capture mode viacuThreadExchangeStreamCaptureModeand restores the thread's previous mode. Capture mode is per thread, so this needs no stream and has no effect when the thread is not capturing. A failed restore is attached to the propagating error as a note, per the error handling policy. This mirrors the libcu++ fix in [libcu++] Make resolving a default memory pool legal under stream capture cccl#11360._cpp/rt/graph.cpp,api.hpp,_rt.pxd,_rt.pyx: newinvalidate_root_graph_state(h_root)retires a hierarchy whose root graph CUDA destroyed itself, so the owning handle's deleter no longer callscuGraphDestroyon it._graph_builder.pyx:GB_end_capturereplacesGB_end_capture_if_neededand settles graph ownership from thecuStreamEndCaptureresult. A NULL graph means the driver discarded the capture graph; the builder drops its handle and enters the newCAPTURE_INVALIDATEDstate.end_building()ends an invalidated capture and raises the driver error,close()closes the builder and then raises, and__dealloc__reports as before.complete(),debug_dot_print(),graph_definition,embed()andGraph.update()reject a graph-less builder with a clear message.end_building()on a forked builder raisesRuntimeErrorinstead of ending capture on the wrong stream, which invalidated the whole capture._graph_builder.pyx:join()closes every fork the loop did not reach before the error propagates, so no fork is left capturing on its private stream. The capture then ends with the driver's unjoined-work error fromend_building(), which the invalidated-capture handling above turns into a clean error instead of a secondcuGraphDestroy(cuda.core: GraphBuilder.join leaves forked builders in a state that segfaults at garbage collection if it raises midway #2776, folded in from cuda.core: close unjoined forks when GraphBuilder.join fails #2881).CUDAWarning; forkedend_building()is rejected; ajoin()that fails partway closes the unjoined forks andend_building()reports the unjoined work.Related Work
join()cleanup as a stacked PR and is folded in here, since its safe failure mode depends on the invalidated-capture handling in this PR.cuMemPoolCreate) during aglobalorthread_localcapture is still refused by the driver.🤖 Generated with Claude Code