fix(cuda.core): move VirtualMemoryResource onto the _rt handle layer - #2917
Conversation
Each physical allocation, address reservation and mapping now lives in a std::shared_ptr handle whose deleter knows the exact driver call to undo it. A buffer owns a range of mappings through its device pointer handle, so everything a buffer maps is released when the last buffer that maps it closes, and a failed multi-step operation unwinds by letting its local handles die. The module moves from Python to Cython. The design is in cuda_core/cuda/core/_cpp/rt/VMM_DESIGN.md. Behavior changes: - modify_allocation returns a new VirtualMemoryBuffer and leaves the input open; the two alias the same physical memory, which is freed when the last of them closes. The pointer is preserved when the driver grants the adjacent address range. - Buffer.size after a grow is the aligned total. - config= applies to the chunk the call adds and is not stored on the resource. - Buffers from allocate() free themselves on close and do not call deallocate(), which now serves pointers wrapped with Buffer.from_handle. - A buffer records the stream passed to allocate(); the last close of an aliased range synchronizes every recorded stream before it unmaps. An explicit close on a capturing stream raises. - location_type="host" requires handle_type=None. allocate(0) returns an empty buffer without a driver call. Fixes NVIDIA#2887 Fixes NVIDIA#2907 Fixes NVIDIA#2908 Fixes NVIDIA#2909 Fixes NVIDIA#2886 Fixes NVIDIA#2345 Addresses NVIDIA#2388 item 2 and the size-0, misaligned-probe and host handle-type parts of NVIDIA#2910. Part of NVIDIA#2906. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The child interpreter inherited pytest's working directory, cuda_core/, so `import cuda.core` resolved to the uncompiled source tree in CI and failed on `cuda.core._version`. Use the shared run_python_snippet helper, which starts the child in an empty temporary directory. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Run the range deleter's stream sync in relaxed capture mode, so a capture on an unrelated stream is not invalidated. - Record a real stream on allocate(0) and inherit it on the grow. - Require handle_type=None for location "host" only. - Apply the constructor's option checks to a per-call modify_allocation config, including the RDMA support check. - Narrow the close() capture contract to non-default streams in the docstring, design doc and release note. - Tests: failed grow leaves the input intact, close during an unrelated capture, GC release during capture, deterministic stream sync with a sleep kernel, cuMemGetAccess on both chunks, graph retention across a grow, forced-move leak on 2 MiB that fails rather than skips. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The struct setter in cuda-bindings 13.0 accepts only the enum, and the Cython helper returns a plain int. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
The no-access case can call this with count == 0 and descs.data() from an empty vector. Constructing access(descs, descs + count) then does pointer arithmetic/range construction on a possibly null pointer. Since self_access=None with no peers is supported, could this special-case zero descriptors before forming the range?
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
Thanks for the correction. The C++17 vector::data contract explicitly requires [data(), data() + size()) to be a valid range, including the empty-vector case, and the dedicated no-access regression covers this path. My concern is resolved.
…d size overflow Add tests for a host-located grow that moves, for a release ordered on the legacy default stream while a blocking stream in its context is capturing, and for a size whose rounding to the granularity does not fit in size_t. The forced-move test now asserts that neither the grow nor the closes warn (NVIDIA#2877). _align_up raises OverflowError instead of wrapping. The docstrings and the release note say that config has no effect when the buffer already covers the request and never changes the access of mapped memory, and that a host-located resource records no default stream. VMM_DESIGN.md describes the close() override. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…SIGN.md List the fifteen properties the handle-based VirtualMemoryResource maintains: once-only and ordered release of reservations and allocations, what a failed or successful grow leaves behind, stream ordering and graph capture, ownership by graph nodes and aliases, per-chunk access, range layout and rounding, the base-address registry, the deallocate() contract, context independence, and interpreter shutdown. The wording names no mechanism, so the list stays valid if the release is made stream-ordered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Independent Jetson Thor / ARM64 validation of head Results on the real CUDA driver:
One qualification: the first non-isolated head run was 38 passed / 1 skipped / 1 failed at This is platform-specific evidence, not an approval of every ownership/API design choice. No Windows/fabric/RDMA execution coverage is claimed. |
|
A broad question about |
|
… the default-token check `VirtualMemoryResource.device` and `.config` are now `cdef readonly`; no other resource in the layer exposes writable attributes. The raw-handle default-stream check moves to the stream module as `Stream_handle_is_default_token`, and `Stream_is_default_token` delegates to it, so the two modules agree on one definition. `VirtualMemoryBuffer. close()` treats an empty handle as "no stream recorded" explicitly instead of folding it into the token check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… free memory The counter from cuMemGetInfo covers the whole device, so any other process moves it and the tests fail on shared machines. Each test now asks the driver about the exact addresses it used after close: the mapping lookup must fail and freeing the reservation must fail because it no longer exists. Closes run under assert_no_cuda_warning, so a failed unmap, address free or release fails the test. The leak test lists one reservation per allocate and one more per grow, for both the in-place and the moved case. One deterministic pass replaces the eight-iteration loop. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A range is now an immutable list of mapping handles that lives in the buffer's device pointer box. A grow copies the input's list, appends or replaces mappings, and builds a new range for its result; the input's range never changes. Mapping handles are shared between ranges, so each mapping unmaps when the last range that holds it goes. Two buffers therefore never share mutable state, which is what made concurrent grows of aliased buffers unsafe: the shared range's mapping vector was appended and iterated without a lock, and a grow that lost a race could dereference a handle another thread had emptied. With one range and one recorded stream per buffer, teardown follows the ordinary Buffer model, so the stream union, the range mutex, the base-address registry and the range header are gone. modify_allocation never returns its input any more. A request the buffer already covers returns a full alias without a driver call, so closing the result never closes the buffer passed in. It also reads the input's handle once, so a close from another thread defers the release instead of emptying what the call reads. The box behind a VMM handle is a VmmDevicePtrBox, a DevicePtrBox with the range as a member and no virtual functions. Every handle on a VirtualMemoryBuffer comes from deviceptr_create_vmm, including the size-zero buffer, which now sits on a VMM box with an empty range, so the class check in modify_allocation is what makes the downcast valid. Adds a test that grows and closes aliases of one buffer from four threads, reduced from the report on this pull request. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ndle deallocation_stream() was the one accessor in the layer that dereferenced an empty handle instead of returning empty, as its sibling set_deallocation_stream and every as_cu() overload do. A buffer closed by one thread while another still reads its handle now gets an empty stream rather than a crash. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Join each worker with the suite's sanitizer-aware timeout and assert that none is still alive before the shared buffers are closed, as the other threading tests do. Drop "undefined" from the docstring. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Since the last round of review, one design change was applied, prompted by the segfault Brandon found, and a few smaller changes came with it. The design change: What came with it:
The PR body and the range section of |
brandon-b-miller
left a comment
There was a problem hiding this comment.
Looking very solid. Couple more Q's
# Conflicts: # cuda_core/docs/source/release/1.3.0-notes.rst
…leter The deleter behind Buffer.from_handle(..., mr) acquired the GIL and called deallocate() while an exception could be propagating through the caller that released the last reference. The handler that reports a failed deallocate() then cleared that exception, and the caller returned an error with no exception set, which Python reports as SystemError. report_message already saved and restored the pending exception inline. PendingExceptionGuard (py.hpp) saves the exception in flight and restores it on scope exit, dropping anything the scope itself raised. The deleter and report_message use it. The regression test releases a temporary buffer whose deallocate() fails while a TypeError propagates and expects the TypeError and a CUDAWarning. Found in review of NVIDIA#2917. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…te the release wait The Python class had __weakref__ as a subclass of an extension type; the cdef class declares it, as Buffer and the pool-backed resources do. VMM_DESIGN.md compared the blocking release to pool-backed deleters, which do not block: cuMemFreeAsync is stream-ordered. The precedent is _SynchronousMemoryResource and LegacyPinnedMemoryResource, which wait in deallocate() on the same deleter path. The design doc, the class docstring, close() and the release note now say that closing a buffer waits for the work on its deallocation stream and how to control when that happens. The release note also drops a sentence about the range deleter synchronizing every recorded stream, which immutable ranges made false. The follow-up for a stream-ordered release is NVIDIA#2989. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolves the conflicts with NVIDIA#2920 (driver calls through cuda-bindings' resolved pointers): - driver_api.hpp: the VMM entry points join the X-macro table with the CUDA versions cuda-bindings requests them at; driver_api.cpp and the pointer-initialization block of _rt.pyx take main's side, since the table is now filled from cuda-bindings. - virtual_memory.cpp: raw p_ calls become DRIVER_CALL, and the cuStreamGetCaptureInfo arity fence branches on CUDA_CORE_BUILD_MAJOR. - py_report.cpp: PendingExceptionGuard plus main's KeyboardInterrupt handling; the identical local guard in py_driver_fns.cpp is replaced by the shared one in py.hpp. - _virtual_memory_resource.py was deleted on this branch; its one change on main (BUILD_CUDA_MAJOR instead of binding_version()) is applied to the .pyx.
…MM deleter cuStreamGetCaptureInfo has two slots in the cuda-bindings loader (v2 and v3) and a different arity per CUDA major, which needed a build-major fence and broke the driver-table test that builds a fake table from the first slot per name. cuStreamIsCapturing is the query that cuStreamGetCaptureInfo makes first: same status, including the implicit capture error for the legacy stream, one slot, one signature. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
Re-reviewed current head 818a440 after the latest review fixes. The two fresh blockers I was tracking are resolved:
- VirtualMemoryResource restores
__weakref__, preserving weak-reference support after the move to a cdef class. - MR-backed Buffer destruction now wraps Python deallocation with
PendingExceptionGuard, and the new regression verifies that a temporary released during exception unwinding preserves the original TypeError while reporting the cleanup failure as CUDAWarning instead of producing SystemError.
The current cross-platform build/test matrix is green. The synchronous VMM-cleanup concern is explicitly tracked separately in #2989 and does not represent a newly introduced correctness regression on this head. No remaining blocker from my review.
Summary
Moves
VirtualMemoryResourceonto the_rthandle layer, as planned in #2906 (design). Each physical allocation, address reservation and mapping is astd::shared_ptrhandle with a deleter that knows the exact driver call to undo it. A buffer owns a range of mappings through its device pointer handle, so everything a buffer maps is released when the last buffer that maps it closes, and a failed multi-step operation unwinds by letting its local handles die. Ranges are immutable: a grow copies the input's mapping list and builds a new range for its result, so two buffers never share mutable state and each buffer owns exactly one range and one recorded stream. The module moves from Python to Cython. The design lands ascuda_core/cuda/core/_cpp/rt/VMM_DESIGN.md.Behavior changes
modify_allocationreturns a newVirtualMemoryBufferand leaves the buffer passed in open. The two alias the same physical memory, which is freed when the last of them closes. The pointer is preserved when the driver grants the adjacent address range. The result is always a new buffer, so closing it never closes the buffer passed in; a request the buffer already covers returns a full alias without a driver call.Buffer.sizeafter a grow is the aligned total.config=applies to the chunk the call adds and is no longer stored on the resource. It must keep the resource's location and passes the constructor's option checks, including the RDMA support check.allocate()free themselves when they close and no longer calldeallocate(), which now serves pointers wrapped withBuffer.from_handle.allocate()and synchronizes it when it closes, before releasing its share of the mappings; a mapping another buffer still holds stays mapped. An explicitclose()on a capturing stream other than a default stream raises; a release ordered on a default stream that would disturb a capture in its context is reported as aCUDAWarningand unmaps without the synchronization. The synchronization runs in relaxed capture mode, so it does not invalidate a capture on an unrelated stream.modify_allocationaccepts only buffers this resource returned.location_type="host"requireshandle_type=None, which the driver requires.allocate(0)returns an empty buffer without a driver call.Testing
cuMemGetAccesson both chunks after aconfig=grow, the per-call config checks, the raw-pointerdeallocate()path, host location without a current context, size zero with an inherited stream, buffers alive at interpreter shutdown, and four threads growing and closing aliases of one buffer at once, reduced from the reproduction in review.close()until the launch completes and the graph is gone. A second test keeps the range mapped across a grow and the close of every alias.cuda_coresuite passed: 4325 passed, 100 skipped, 1 xfailed, 0 failed. The VMM selection passed in fixed order (40 passed, 1 skipped for GPUDirect RDMA) and twice in random order, and the reproduction script from review runs to completion without a crash.Issues
Fixes #2887
Fixes #2907
Fixes #2908
Fixes #2909
Fixes #2886
Fixes #2345
Fixes #2877
Addresses #2388 items 1, 2 and 3 (the rollback that lost access grants, the dead fast path, and the finalizer warnings; item 4 landed in #2418) and the size-0, misaligned-probe and host handle-type parts of #2910. Part of #2906. Supersedes #2880, #2889 and #2237: they edit the module this PR replaces, and the fixes they carried (#2877, #2886, #2345) are part of the redesign.
🤖 Generated with Claude Code