Skip to content

Pool-backed MemoryResource buffers bypass overridden deallocate() methods #2615

Open

Description

@Andy-Jost

Summary

Buffers allocated by pool-backed memory resources are freed directly by their C++ DevicePtrHandle deleter. Their associated memory resource's deallocate() method is never called.

This makes deallocation behavior inconsistent and prevents subclasses of DeviceMemoryResource, PinnedMemoryResource, and ManagedMemoryResource from observing or customizing teardown.

Reproducer

from cuda.core import Device, DeviceMemoryResource

calls = []


class RecordingMemoryResource(DeviceMemoryResource):
    def deallocate(self, ptr, size, *, stream):
        calls.append((ptr, size, stream))
        return super().deallocate(ptr, size, stream=stream)


device = Device()
device.set_current()

mr = RecordingMemoryResource(device)
buf = mr.allocate(1024, stream=device.default_stream)

assert buf.memory_resource is mr
buf.close()

assert len(calls) == 1  # Fails: calls is empty

The allocation is freed, but directly through the C++ handle rather than through RecordingMemoryResource.deallocate().

Inconsistent behavior

The behavior depends on how the Buffer was constructed:

  • mr.allocate(...) uses a directly owning C++ handle and bypasses mr.deallocate().
  • Buffer.from_handle(..., mr=mr) records the MR as the owner and dispatches teardown through mr.deallocate().

Consequently, the same memory resource subclass has two different deallocation models.

Callback-backed resources such as LegacyPinnedMemoryResource also dispatch through mr.deallocate(), making the behavior inconsistent between built-in resource implementations.

Expected behavior

If concrete memory resources are supported as subclass bases, an overridden deallocate() should be honored for buffers allocated by the subclass.

This is useful for accounting, tracing, validation, failure injection, and custom resource policies—not only test instrumentation.

If concrete memory resources are intentionally not subclassable, that restriction should instead be explicit and enforced.

Implementation considerations

The current direct C++ path has important properties that should be preserved:

  • It retains the memory-pool handle for the allocation lifetime.
  • It can free without executing Python during interpreter shutdown.
  • It stores the stream/context recipe needed for safe deferred teardown.

A fix may therefore require a callback-capable pool allocation handle that retains the pool while dispatching through the Python MR, or a fast path for exact built-in types with callback dispatch for subclasses.

GraphMemoryResource, which also creates directly owning handles, should be audited for the same behavior.

Related

Activity

  1. added
    bugSomething isn't working
    cuda.coreEverything related to the cuda.core module
    on Aug 13, 2026
  2. self-assigned this
    on Aug 13, 2026
  3. added theissue type on Aug 13, 2026
  4. added this to the cuda.core next milestone on Aug 13, 2026
  5. atiaomar1978-hub commented on Aug 13, 2026

    @atiaomar1978-hub
    Contributor

    Opened PR #2620 for this issue.

    The fix uses MR-owned device pointer handles for subclasses so overridden deallocate() is called consistently with Buffer.from_handle(mr=...). Built-in pool/graph MR types retain the direct C++ deleter for shutdown-safe nogil frees.

    Includes regression tests for close/GC/stream forwarding on pool-backed subclasses.

  6. atiaomar1978-hub commented on Aug 13, 2026

    @atiaomar1978-hub
    Contributor

    Hi @Andy-Jost — I've opened PR #2620 for this issue.

    When you have a moment, could a maintainer please:

    • Assign the PR (and/or this issue) as appropriate
    • Set the cuda.core next milestone on the PR

    I don't have upstream permissions to set assignee/milestone on NVIDIA/cuda-python PRs. The cuda.core label is already applied.

    Happy to adjust based on your feedback. Thanks!

  7. atiaomar1978-hub commented on Aug 14, 2026

    @atiaomar1978-hub
    Contributor

    Validation update (GPU tests on RunPod)

    I validated the fix in PR #2620 on real NVIDIA hardware outside CI.

    Setup: RTX 4000 Ada (RunPod community), branch fix/pool-backed-mr-deallocate-2615, editable cuda_core build against CUDA 13 bindings/headers.

    Targeted run:

    pytest cuda_core/tests/test_memory.py \
      -k "pool_backed_mr or dmr_deallocate_frees_pool_pointer or dmr_from_handle_deallocate or deallocate" -v

    Outcome: 11 passed, 2 skipped, 0 failed

    The pool-backed MR tests confirm that overridden deallocate() is now invoked on close/GC and receives the allocation stream, and that pool pointers are freed through the MR path (test_dmr_deallocate_frees_pool_pointer, test_dmr_from_handle_deallocate_frees_pool_pointer).

    This addresses the core issue described here: pool-backed buffers no longer bypass Python deallocate() overrides.

    Full details: #2620 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcuda.coreEverything related to the cuda.core module

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions