Skip to content

Commit 5cd9afd

Browse files
fix(cuda.core): route pool-backed MR buffers through deallocate()
Pool-backed and graph memory resources now wrap raw allocations with MR-owned device pointer handles, matching Buffer.from_handle(mr=...). Subclasses can observe or customize teardown via deallocate(). Fixes #2615 Signed-off-by: Omar Atie <atiaomar1978-hub@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 4736d4a commit 5cd9afd

5 files changed

Lines changed: 154 additions & 31 deletions

File tree

cuda_core/cuda/core/_memory/_buffer.pxd

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from libc.stddef cimport size_t
6+
57
from libcpp cimport bool as cpp_bool
68
from libcpp.atomic cimport atomic as std_atomic
79

10+
from cuda.bindings cimport cydriver
811
from cuda.core._resource_handles cimport DevicePtrHandle
12+
from cuda.core._stream cimport Stream
913

1014

1115
cdef struct _MemAttrs:
@@ -46,6 +50,15 @@ cdef Buffer Buffer_from_deviceptr_handle(
4650
)
4751

4852

53+
# Wrap a raw device pointer with MR-owned teardown and record the stream.
54+
cdef DevicePtrHandle deviceptr_create_owned_by_mr(
55+
cydriver.CUdeviceptr ptr,
56+
size_t size,
57+
object mr,
58+
Stream stream,
59+
) except *
60+
61+
4962
# Shared argument coercion for the batched free functions (copy_batch,
5063
# prefetch_batch, discard_batch, discard_prefetch_batch). `single_hint`
5164
# names the per-buffer API to use instead when a bare Buffer is passed.

cuda_core/cuda/core/_memory/_buffer.pyx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,22 @@ cdef inline void _apply_deallocation_stream(
9292
HANDLE_RETURN(status)
9393

9494

95+
cdef DevicePtrHandle deviceptr_create_owned_by_mr(
96+
cydriver.CUdeviceptr ptr,
97+
size_t size,
98+
object mr,
99+
Stream stream,
100+
) except *:
101+
"""Create an MR-owned device pointer handle with a recorded deallocation stream."""
102+
cdef DevicePtrHandle h_ptr = deviceptr_create_with_mr(ptr, size, mr)
103+
try:
104+
_apply_deallocation_stream(h_ptr, stream._h_stream)
105+
except BaseException:
106+
h_ptr.reset()
107+
raise
108+
return h_ptr
109+
110+
95111
__all__ = ['Buffer', 'MemoryResource']
96112

97113

cuda_core/cuda/core/_memory/_graph_memory_resource.pyx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ from __future__ import annotations
77
from libc.stdint cimport intptr_t
88

99
from cuda.bindings cimport cydriver
10-
from cuda.core._memory._buffer cimport Buffer, Buffer_from_deviceptr_handle, MemoryResource
10+
from cuda.core._memory._buffer cimport (
11+
Buffer,
12+
Buffer_from_deviceptr_handle,
13+
MemoryResource,
14+
deviceptr_create_owned_by_mr,
15+
)
1116
from cuda.core._resource_handles cimport (
1217
DevicePtrHandle,
13-
deviceptr_alloc_async,
14-
get_last_error,
1518
as_cu,
1619
)
1720

@@ -210,18 +213,12 @@ cdef inline int check_capturing(cydriver.CUstream s) except?-1 nogil:
210213

211214
cdef inline Buffer GMR_allocate(cyGraphMemoryResource self, size_t size, Stream stream):
212215
cdef cydriver.CUstream s = as_cu(stream._h_stream)
216+
cdef cydriver.CUdeviceptr ptr
213217
cdef DevicePtrHandle h_ptr
214218
with nogil:
215219
check_capturing(s)
216-
h_ptr = deviceptr_alloc_async(size, stream._h_stream)
217-
if not h_ptr:
218-
HANDLE_RETURN(get_last_error())
219-
raise RuntimeError(
220-
f"Failed to allocate {size} bytes from GraphMemoryResource: "
221-
"cuda-core returned an empty allocation handle without recording a CUDA error. "
222-
"This is an internal cuda-core error; please report it with your CUDA driver, "
223-
"CUDA Toolkit, and cuda-python versions."
224-
)
220+
HANDLE_RETURN(cydriver.cuMemAllocAsync(&ptr, size, s))
221+
h_ptr = deviceptr_create_owned_by_mr(ptr, size, self, stream)
225222
return Buffer_from_deviceptr_handle(h_ptr, size, self, None)
226223

227224

cuda_core/cuda/core/_memory/_memory_pool.pyx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@ from libc.stdint cimport uintptr_t
1010
from libc.string cimport memset
1111

1212
from cuda.bindings cimport cydriver
13-
from cuda.core._memory._buffer cimport Buffer, Buffer_from_deviceptr_handle, MemoryResource
13+
from cuda.core._memory._buffer cimport (
14+
Buffer,
15+
Buffer_from_deviceptr_handle,
16+
MemoryResource,
17+
deviceptr_create_owned_by_mr,
18+
)
1419
from cuda.core._memory cimport _ipc
1520
from cuda.core._stream cimport Stream_accept, Stream
1621
from cuda.core._resource_handles cimport (
1722
MemoryPoolHandle,
1823
DevicePtrHandle,
1924
create_mempool_handle,
20-
deviceptr_alloc_from_pool,
2125
get_last_error,
2226
as_cu,
2327
as_py,
@@ -330,18 +334,12 @@ cdef inline int check_not_capturing(cydriver.CUstream s) except?-1 nogil:
330334

331335
cdef Buffer _MP_allocate(_MemPool self, size_t size, Stream stream, type cls = Buffer):
332336
cdef cydriver.CUstream s = as_cu(stream._h_stream)
337+
cdef cydriver.CUdeviceptr ptr
333338
cdef DevicePtrHandle h_ptr
334339
with nogil:
335340
check_not_capturing(s)
336-
h_ptr = deviceptr_alloc_from_pool(size, self._h_pool, stream._h_stream)
337-
if not h_ptr:
338-
HANDLE_RETURN(get_last_error())
339-
raise RuntimeError(
340-
f"Failed to allocate {size} bytes from {self.__class__.__name__}: "
341-
"cuda-core returned an empty allocation handle without recording a CUDA error. "
342-
"This is an internal cuda-core error; please report it with your CUDA driver, "
343-
"CUDA Toolkit, and cuda-python versions."
344-
)
341+
HANDLE_RETURN(cydriver.cuMemAllocFromPoolAsync(&ptr, size, as_cu(self._h_pool), s))
342+
h_ptr = deviceptr_create_owned_by_mr(ptr, size, self, stream)
345343
return Buffer_from_deviceptr_handle(h_ptr, size, self, None, cls)
346344

347345

cuda_core/tests/test_memory.py

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,94 @@ def deallocate(self, ptr, size, *, stream=None):
515515
assert received["stream"].handle == stream.handle
516516

517517

518+
class _RecordingPoolMR(DeviceMemoryResource):
519+
"""Pool-backed MR that records deallocate() invocations."""
520+
521+
def __init__(self, device, options=None):
522+
super().__init__(device, options)
523+
self.dealloc_calls = []
524+
525+
def deallocate(self, ptr, size, *, stream):
526+
self.dealloc_calls.append((ptr, size, stream))
527+
super().deallocate(ptr, size, stream=stream)
528+
529+
530+
@pytest.mark.agent_authored(model="cursor-grok-4.6")
531+
@pytest.mark.parametrize("pinned", [False, True], ids=["device", "pinned"])
532+
def test_pool_backed_mr_deallocate_called_on_close(mempool_device, pinned):
533+
"""Pool-backed mr.allocate() honors overridden deallocate() on close (#2615)."""
534+
dev = mempool_device
535+
stream = dev.default_stream
536+
if pinned:
537+
skip_if_pinned_memory_unsupported(dev)
538+
539+
class RecordingMR(PinnedMemoryResource):
540+
def __init__(self, options=None):
541+
super().__init__(options)
542+
self.dealloc_calls = []
543+
544+
def deallocate(self, ptr, size, *, stream):
545+
self.dealloc_calls.append((ptr, size, stream))
546+
super().deallocate(ptr, size, stream=stream)
547+
548+
mr = RecordingMR(PinnedMemoryResourceOptions(max_size=POOL_SIZE))
549+
else:
550+
551+
class RecordingMR(DeviceMemoryResource):
552+
def __init__(self, device, options=None):
553+
super().__init__(device, options)
554+
self.dealloc_calls = []
555+
556+
def deallocate(self, ptr, size, *, stream):
557+
self.dealloc_calls.append((ptr, size, stream))
558+
super().deallocate(ptr, size, stream=stream)
559+
560+
mr = RecordingMR(dev, DeviceMemoryResourceOptions(max_size=POOL_SIZE))
561+
562+
buf = mr.allocate(1024, stream=stream)
563+
assert buf.memory_resource is mr
564+
assert len(mr.dealloc_calls) == 0
565+
buf.close(stream=stream)
566+
stream.sync()
567+
assert len(mr.dealloc_calls) == 1
568+
assert mr.dealloc_calls[0][1] == 1024
569+
570+
571+
@pytest.mark.agent_authored(model="cursor-grok-4.6")
572+
def test_pool_backed_mr_deallocate_called_on_gc(mempool_device):
573+
"""Pool-backed mr.allocate() honors overridden deallocate() on GC (#2615)."""
574+
import gc
575+
576+
dev = mempool_device
577+
stream = dev.default_stream
578+
mr = _RecordingPoolMR(dev, DeviceMemoryResourceOptions(max_size=POOL_SIZE))
579+
buf = mr.allocate(1024, stream=stream)
580+
assert len(mr.dealloc_calls) == 0
581+
del buf
582+
gc.collect()
583+
stream.sync()
584+
assert len(mr.dealloc_calls) == 1
585+
586+
587+
@pytest.mark.agent_authored(model="cursor-grok-4.6")
588+
def test_pool_backed_mr_deallocate_receives_stream(mempool_device):
589+
"""Pool-backed mr.allocate() forwards close(stream) to deallocate() (#2615)."""
590+
dev = mempool_device
591+
stream = dev.create_stream()
592+
received = {}
593+
594+
class StreamCapturePoolMR(_RecordingPoolMR):
595+
def deallocate(self, ptr, size, *, stream):
596+
received["stream"] = stream
597+
super().deallocate(ptr, size, stream=stream)
598+
599+
mr = StreamCapturePoolMR(dev, DeviceMemoryResourceOptions(max_size=POOL_SIZE))
600+
buf = mr.allocate(1024, stream=stream)
601+
buf.close(stream=stream)
602+
stream.sync()
603+
assert received["stream"].handle == stream.handle
604+
605+
518606
@pytest.mark.parametrize("buffer_type", [Buffer, ManagedBuffer])
519607
def test_from_handle_mr_records_default_stream(buffer_type):
520608
"""When a Buffer/ManagedBuffer is minted via :meth:`from_handle` with ``mr``
@@ -2304,25 +2392,36 @@ def test_dmr_handle_and_ownership(mempool_device):
23042392

23052393
@pytest.mark.agent_authored(model="claude-opus-4.8")
23062394
def test_dmr_deallocate_frees_pool_pointer(mempool_device):
2307-
"""Closing a Buffer.from_handle(..., mr=mr) view frees the pointer via the Python
2308-
_MemPool.deallocate path; the pool's in-use bytes drop back."""
2395+
"""Closing a buffer from mr.allocate() frees via deallocate() and returns pool bytes."""
2396+
dev = mempool_device
2397+
stream = dev.default_stream
2398+
mr = DeviceMemoryResource(dev, DeviceMemoryResourceOptions(max_size=POOL_SIZE))
2399+
size = 256
2400+
buf = mr.allocate(size, stream=stream)
2401+
stream.sync()
2402+
used_after_alloc = mr.attributes.used_mem_current
2403+
assert used_after_alloc >= size
2404+
buf.close(stream=stream)
2405+
stream.sync()
2406+
assert int(buf.handle) == 0
2407+
assert mr.attributes.used_mem_current < used_after_alloc
2408+
2409+
2410+
@pytest.mark.agent_authored(model="cursor-grok-4.6")
2411+
def test_dmr_from_handle_deallocate_frees_pool_pointer(mempool_device):
2412+
"""Buffer.from_handle(..., mr=mr) also routes teardown through deallocate()."""
23092413
dev = mempool_device
23102414
stream = dev.default_stream
23112415
mr = DeviceMemoryResource(dev, DeviceMemoryResourceOptions(max_size=POOL_SIZE))
23122416
size = 256
2313-
# Raw pool allocation owned by nobody else, so exactly one owner frees it (no
2314-
# double free); a Buffer.from_handle view then routes teardown through the
2315-
# Python deallocate path that mr.allocate()'s C++-direct free would skip.
23162417
ptr = handle_return(driver.cuMemAllocFromPoolAsync(size, mr.handle, stream.handle))
23172418
stream.sync()
23182419
used_after_alloc = mr.attributes.used_mem_current
23192420
assert used_after_alloc >= size
23202421
buf = Buffer.from_handle(int(ptr), size, mr=mr)
2321-
buf.close(stream)
2422+
buf.close(stream=stream)
23222423
stream.sync()
23232424
assert int(buf.handle) == 0
2324-
# In-use bytes fell back, so the pointer was actually returned (buf.handle == 0
2325-
# alone wouldn't prove it: the deleter callback swallows a failed free).
23262425
assert mr.attributes.used_mem_current < used_after_alloc
23272426

23282427

0 commit comments

Comments
 (0)