@@ -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 ])
519607def 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" )
23062394def 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