Skip to content

Commit fec0ca4

Browse files
committed
fix(cuda.core): make buffer cleanup context-safe
Retain and restore the CUDA context required by deferred buffer cleanup so destruction does not depend on ambient thread state.
1 parent 52ee6ea commit fec0ca4

15 files changed

Lines changed: 407 additions & 201 deletions

‎cuda_core/cuda/core/_cpp/resource_handles.cpp‎

Lines changed: 166 additions & 129 deletions
Large diffs are not rendered by default.

‎cuda_core/cuda/core/_cpp/resource_handles.hpp‎

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,10 @@ void clear_last_error() noexcept;
6666

6767
extern decltype(&cuDevicePrimaryCtxRetain) p_cuDevicePrimaryCtxRetain;
6868
extern decltype(&cuDevicePrimaryCtxRelease) p_cuDevicePrimaryCtxRelease;
69+
extern decltype(&cuDevicePrimaryCtxGetState) p_cuDevicePrimaryCtxGetState;
6970
extern decltype(&cuCtxGetCurrent) p_cuCtxGetCurrent;
7071
extern decltype(&cuCtxSetCurrent) p_cuCtxSetCurrent;
72+
extern decltype(&cuCtxGetDevice) p_cuCtxGetDevice;
7173
extern decltype(&cuGreenCtxCreate) p_cuGreenCtxCreate;
7274
extern decltype(&cuGreenCtxDestroy) p_cuGreenCtxDestroy;
7375
extern decltype(&cuCtxFromGreenCtx) p_cuCtxFromGreenCtx;
@@ -232,7 +234,16 @@ GreenCtxHandle create_green_ctx_handle_ref(CUgreenCtx ctx);
232234
// Returns empty handle on error (caller must check)
233235
ContextHandle get_primary_context(int device_id);
234236

235-
// Get handle to the current CUDA context
237+
// Create a context handle that owns `ctx` when the driver allows it.
238+
// cuDevicePrimaryCtxRetain is the only lifetime-extending context API, so an
239+
// owning handle is returned only for a device's primary context (or for a
240+
// context cuda.core already tracks). Otherwise this degrades to
241+
// create_context_handle_ref and the caller must keep the context alive.
242+
// Returns empty handle if ctx is null.
243+
ContextHandle retain_context_handle(CUcontext ctx);
244+
245+
// Get handle to the current CUDA context, owning it when possible
246+
// (see retain_context_handle).
236247
// Returns empty handle if no context is current (caller must check)
237248
ContextHandle get_current_context();
238249

@@ -354,12 +365,16 @@ using DevicePtrHandle = std::shared_ptr<const CUdeviceptr>;
354365
DevicePtrHandle deviceptr_alloc_from_pool(
355366
size_t size,
356367
const MemoryPoolHandle& h_pool,
357-
const StreamHandle& h_stream);
368+
const StreamHandle& h_stream,
369+
const ContextHandle& h_release_context);
358370

359371
// Allocate device memory asynchronously via cuMemAllocAsync.
360372
// When the last reference is released, cuMemFreeAsync is called on the stored stream.
361373
// Returns empty handle on error (caller must check).
362-
DevicePtrHandle deviceptr_alloc_async(size_t size, const StreamHandle& h_stream);
374+
DevicePtrHandle deviceptr_alloc_async(
375+
size_t size,
376+
const StreamHandle& h_stream,
377+
const ContextHandle& h_release_context);
363378

364379
// Allocate device memory synchronously via cuMemAlloc.
365380
// When the last reference is released, cuMemFree is called.
@@ -390,7 +405,8 @@ DevicePtrHandle deviceptr_create_with_owner(CUdeviceptr ptr, PyObject* owner);
390405
DevicePtrHandle deviceptr_create_mapped_graphics(
391406
CUdeviceptr ptr,
392407
const GraphicsResourceHandle& h_resource,
393-
const StreamHandle& h_stream);
408+
const StreamHandle& h_stream,
409+
const ContextHandle& h_release_context);
394410

395411
// Callback type for MemoryResource deallocation.
396412
// Called from the shared_ptr deleter when a handle created via
@@ -404,36 +420,39 @@ using MRDeallocCallback = void (*)(PyObject* mr, CUdeviceptr ptr,
404420
void register_mr_dealloc_callback(MRDeallocCallback cb);
405421

406422
// Create a device pointer handle whose destructor calls mr.deallocate()
407-
// via the registered callback. The supplied allocation context is retained,
408-
// and the optional deallocation stream is passed to the callback. The mr's
409-
// refcount is incremented and decremented when the handle is released.
423+
// via the registered callback. The supplied allocation context becomes a
424+
// dependency of the handle. h_stream is passed to the callback unchanged, and
425+
// h_release_context is made current while the callback runs. The mr's refcount
426+
// is incremented and decremented when the handle is released.
410427
// If mr is nullptr, equivalent to deviceptr_create_ref.
411428
DevicePtrHandle deviceptr_create_with_mr(
412429
CUdeviceptr ptr,
413430
size_t size,
414431
PyObject* mr,
415432
const ContextHandle& h_allocation_context,
416433
const StreamHandle& h_stream,
417-
bool require_stream_context);
434+
const ContextHandle& h_release_context);
418435

419436
// Import a device pointer from IPC via cuMemPoolImportPointer.
420437
// When the last reference is released, cuMemFreeAsync is called on the stored stream.
438+
// h_release_context is made current for that call.
421439
// Note: Does not yet implement reference counting for nvbug 5570902.
422440
// On error, returns empty handle and sets thread-local error (use get_last_error()).
423441
DevicePtrHandle deviceptr_import_ipc(
424442
const MemoryPoolHandle& h_pool,
425443
const void* export_data,
426-
const StreamHandle& h_stream);
444+
const StreamHandle& h_stream,
445+
const ContextHandle& h_release_context);
427446

428447
// Access the deallocation stream for a device pointer handle (read-only).
429448
// For non-owning handles, the stream is not used but can still be accessed.
430449
StreamHandle deallocation_stream(const DevicePtrHandle& h) noexcept;
431450

432-
// Set the deallocation stream for a device pointer handle. Context-relative
433-
// default streams are bound to the calling thread's current context.
434-
CUresult set_deallocation_stream(
451+
// Set the stream and context to use when the device pointer is released.
452+
void set_deallocation_stream(
435453
const DevicePtrHandle& h,
436-
const StreamHandle& h_stream) noexcept;
454+
const StreamHandle& h_stream,
455+
const ContextHandle& h_release_context) noexcept;
437456

438457
// ============================================================================
439458
// Library handle functions

‎cuda_core/cuda/core/_graphics.pyx‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ from typing import Sequence
88

99
from cuda.bindings cimport cydriver
1010
from cuda.core._resource_handles cimport (
11+
ContextHandle,
1112
DevicePtrHandle,
1213
create_graphics_resource_handle,
1314
deviceptr_create_mapped_graphics,
@@ -16,7 +17,11 @@ from cuda.core._resource_handles cimport (
1617
as_intptr,
1718
)
1819
from cuda.core._memory._buffer cimport Buffer, Buffer_from_deviceptr_handle
19-
from cuda.core._stream cimport Stream, Stream_accept
20+
from cuda.core._stream cimport (
21+
Stream,
22+
Stream_accept,
23+
Stream_resolve_context,
24+
)
2025
from cuda.core._utils.cuda_utils cimport HANDLE_RETURN
2126

2227
__all__ = ['GraphicsResource']
@@ -281,6 +286,9 @@ cdef class GraphicsResource:
281286
cdef Stream s_obj = Stream_accept(stream)
282287
cdef cydriver.CUgraphicsResource raw = as_cu(self._handle)
283288
cdef cydriver.CUstream cy_stream = as_cu(s_obj._h_stream)
289+
cdef ContextHandle h_release_context = Stream_resolve_context(s_obj)
290+
if not h_release_context:
291+
HANDLE_RETURN(cydriver.CUresult.CUDA_ERROR_INVALID_CONTEXT)
284292
try:
285293
with nogil:
286294
HANDLE_RETURN(
@@ -300,7 +308,11 @@ cdef class GraphicsResource:
300308
HANDLE_RETURN(rollback_status)
301309
raise
302310
h_ptr = deviceptr_create_mapped_graphics(
303-
dev_ptr, self._handle, s_obj._h_stream)
311+
dev_ptr,
312+
self._handle,
313+
s_obj._h_stream,
314+
h_release_context,
315+
)
304316
if not h_ptr:
305317
status = get_last_error()
306318
with nogil:

‎cuda_core/cuda/core/_memory/_buffer.pyi‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ class Buffer:
4545
When ``mr`` is provided, the buffer takes ownership: ``mr.deallocate()``
4646
is called when the buffer is closed or garbage collected. When ``owner``
4747
is provided, the owner is kept alive but no deallocation is performed.
48+
49+
An owning buffer always records a deallocation stream, defaulting to
50+
``default_stream()``, so teardown never has to choose one at destruction
51+
time. Callers wanting different ordering must pass ``stream``.
4852
"""
4953

5054
@staticmethod
@@ -73,9 +77,11 @@ class Buffer:
7377
The reference is kept as long as the buffer is alive.
7478
The ``owner`` and ``mr`` cannot be specified together.
7579
stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`, optional
76-
Initial deallocation stream when ``mr`` owns the pointer. If
77-
omitted, the current default stream is selected and retained now,
78-
rather than during eventual destruction.
80+
Deallocation stream to record when ``mr`` owns the pointer.
81+
Defaults to the calling thread's default stream, chosen here
82+
rather than at destruction time. Note that a default stream is
83+
recorded as a token: under per-thread default streams the driver
84+
resolves it against whichever thread runs the destructor.
7985
8086
Note
8187
----

‎cuda_core/cuda/core/_memory/_buffer.pyx‎

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ from cuda.core._resource_handles cimport (
1717
ContextHandle,
1818
DevicePtrHandle,
1919
StreamHandle,
20-
create_context_handle_ref,
20+
retain_context_handle,
2121
deviceptr_create_with_owner,
2222
deviceptr_create_with_mr,
2323
get_last_error,
@@ -28,7 +28,12 @@ from cuda.core._resource_handles cimport (
2828
)
2929
from cuda.core.typing import DevicePointerType
3030

31-
from cuda.core._stream cimport Stream, Stream_accept, default_stream
31+
from cuda.core._stream cimport (
32+
Stream,
33+
Stream_accept,
34+
Stream_resolve_context,
35+
default_stream,
36+
)
3237
from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value
3338

3439
import sys
@@ -55,10 +60,19 @@ cdef void _mr_dealloc_callback(
5560
"""Called by the C++ deleter to deallocate via MemoryResource.deallocate.
5661
5762
This is the C++ teardown path: there is no Python caller frame from
58-
which to obtain a stream. The device-pointer handle therefore carries
59-
the exact stream selected at allocation/adoption or by ``Buffer.close``.
60-
An empty handle remains ``None``; cleanup must never invent ordering by
61-
selecting a default stream at destruction time.
63+
which to obtain a stream, so the handle supplies the stream recorded at
64+
allocation/adoption or by ``Buffer.close`` and cleanup never picks one of
65+
its own. An empty handle stays ``None``, which is a warning-only path: a
66+
resource that cannot free without a stream raises, and the failure is
67+
reported below rather than propagated, because a destructor has nowhere to
68+
raise to.
69+
70+
The recorded stream may be a default-stream token. A token is resolved by
71+
the driver on the thread that runs the destructor, so under per-thread
72+
default streams the physical stream can differ from the one the buffer's
73+
owner used. Only the choice of stream is preserved; ordering work across
74+
threads remains the application's responsibility, as it is for the
75+
equivalent raw driver calls.
6276
"""
6377
cdef object stream
6478
try:
@@ -76,13 +90,18 @@ __all__ = ['Buffer', 'MemoryResource']
7690

7791
cdef ContextHandle _pointer_context_handle(
7892
cydriver.CUdeviceptr ptr,
79-
bint* require_stream_context,
93+
bint* is_stream_ordered,
8094
):
81-
"""Return the pointer's owning context when CUDA reports one."""
95+
"""Return a handle for the pointer's owning context, if CUDA reports one.
96+
97+
The handle owns the context when the driver permits it (see
98+
``retain_context_handle``), which for a primary context makes the
99+
allocation's context a genuine dependency of the buffer.
100+
"""
82101
cdef ContextHandle h_context
83102
cdef cydriver.CUcontext context = NULL
84103
cdef cydriver.CUresult result
85-
require_stream_context[0] = False
104+
is_stream_ordered[0] = False
86105
if ptr == 0:
87106
return h_context
88107

@@ -94,9 +113,9 @@ cdef ContextHandle _pointer_context_handle(
94113
)
95114
if result == cydriver.CUDA_SUCCESS:
96115
if context != NULL:
97-
return create_context_handle_ref(context)
116+
return retain_context_handle(context)
98117
# Stream-ordered allocations are context-independent.
99-
require_stream_context[0] = True
118+
is_stream_ordered[0] = True
100119
return h_context
101120
if result in (
102121
cydriver.CUresult.CUDA_ERROR_INVALID_VALUE,
@@ -216,28 +235,38 @@ cdef class Buffer:
216235
When ``mr`` is provided, the buffer takes ownership: ``mr.deallocate()``
217236
is called when the buffer is closed or garbage collected. When ``owner``
218237
is provided, the owner is kept alive but no deallocation is performed.
238+
239+
An owning buffer always records a deallocation stream, defaulting to
240+
``default_stream()``, so teardown never has to choose one at destruction
241+
time. Callers wanting different ordering must pass ``stream``.
219242
"""
220243
if mr is not None and owner is not None:
221244
raise ValueError("owner and memory resource cannot be both specified together")
222245
cdef Buffer self = Buffer.__new__(cls)
223246
cdef uintptr_t c_ptr = <uintptr_t>(int(ptr))
224-
cdef ContextHandle h_context
247+
cdef ContextHandle h_allocation_context
248+
cdef ContextHandle h_release_context
225249
cdef StreamHandle h_stream
226250
cdef Stream s
227-
cdef bint require_stream_context
251+
cdef bint is_stream_ordered
228252
if mr is not None:
229-
if stream is not None:
230-
s = Stream_accept(stream)
231-
h_stream = s._h_stream
232-
h_context = _pointer_context_handle(
233-
c_ptr, &require_stream_context)
253+
s = Stream_accept(default_stream() if stream is None else stream)
254+
h_stream = s._h_stream
255+
h_allocation_context = _pointer_context_handle(
256+
c_ptr, &is_stream_ordered)
257+
h_release_context = Stream_resolve_context(s)
258+
if not h_release_context:
259+
h_release_context = h_allocation_context
260+
if is_stream_ordered and not h_release_context:
261+
HANDLE_RETURN(
262+
cydriver.CUresult.CUDA_ERROR_INVALID_CONTEXT)
234263
self._h_ptr = deviceptr_create_with_mr(
235264
c_ptr,
236265
size,
237266
mr,
238-
h_context,
267+
h_allocation_context,
239268
h_stream,
240-
require_stream_context,
269+
h_release_context,
241270
)
242271
if not self._h_ptr:
243272
HANDLE_RETURN(get_last_error())
@@ -289,18 +318,18 @@ cdef class Buffer:
289318
The reference is kept as long as the buffer is alive.
290319
The ``owner`` and ``mr`` cannot be specified together.
291320
stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`, optional
292-
Initial deallocation stream when ``mr`` owns the pointer. If
293-
omitted, the current default stream is selected and retained now,
294-
rather than during eventual destruction.
321+
Deallocation stream to record when ``mr`` owns the pointer.
322+
Defaults to the calling thread's default stream, chosen here
323+
rather than at destruction time. Note that a default stream is
324+
recorded as a token: under per-thread default streams the driver
325+
resolves it against whichever thread runs the destructor.
295326

296327
Note
297328
----
298329
When neither ``mr`` nor ``owner`` is specified, this creates a
299330
non-owning reference. The pointer will NOT be freed when the
300331
:class:`Buffer` is closed or garbage collected.
301332
"""
302-
if mr is not None and stream is None:
303-
stream = default_stream()
304333
return Buffer._init(
305334
ptr, size, mr=mr, owner=owner, stream=stream)
306335

@@ -680,14 +709,19 @@ cdef Buffer Buffer_from_deviceptr_handle(
680709
cdef inline void Buffer_close(Buffer self, object stream):
681710
"""Close a buffer, freeing its memory."""
682711
cdef Stream s
712+
cdef ContextHandle h_release_context
683713
if not self._h_ptr:
684714
return
685715
# Update deallocation stream if provided
686716
if stream is not None:
687717
s = Stream_accept(stream)
718+
h_release_context = Stream_resolve_context(s)
719+
if not h_release_context:
720+
HANDLE_RETURN(
721+
cydriver.CUresult.CUDA_ERROR_INVALID_CONTEXT)
688722
with nogil:
689-
HANDLE_RETURN(set_deallocation_stream(
690-
self._h_ptr, s._h_stream))
723+
set_deallocation_stream(
724+
self._h_ptr, s._h_stream, h_release_context)
691725
# Reset handle - RAII deleter will free the memory (and release owner ref in C++)
692726
self._h_ptr.reset()
693727
self._size = 0

‎cuda_core/cuda/core/_memory/_graph_memory_resource.pyx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@ from libc.stdint cimport intptr_t
99
from cuda.bindings cimport cydriver
1010
from cuda.core._memory._buffer cimport Buffer, Buffer_from_deviceptr_handle, MemoryResource
1111
from cuda.core._resource_handles cimport (
12+
ContextHandle,
1213
DevicePtrHandle,
1314
deviceptr_alloc_async,
1415
get_last_error,
1516
as_cu,
1617
)
1718

18-
from cuda.core._stream cimport Stream_accept, Stream
19+
from cuda.core._stream cimport (
20+
Stream,
21+
Stream_accept,
22+
Stream_resolve_context,
23+
)
1924
from cuda.core._utils.cuda_utils cimport HANDLE_RETURN
2025

2126
from typing import TYPE_CHECKING
@@ -210,10 +215,14 @@ cdef inline int check_capturing(cydriver.CUstream s) except?-1 nogil:
210215

211216
cdef inline Buffer GMR_allocate(cyGraphMemoryResource self, size_t size, Stream stream):
212217
cdef cydriver.CUstream s = as_cu(stream._h_stream)
218+
cdef ContextHandle h_release_context = Stream_resolve_context(stream)
219+
if not h_release_context:
220+
HANDLE_RETURN(cydriver.CUresult.CUDA_ERROR_INVALID_CONTEXT)
213221
cdef DevicePtrHandle h_ptr
214222
with nogil:
215223
check_capturing(s)
216-
h_ptr = deviceptr_alloc_async(size, stream._h_stream)
224+
h_ptr = deviceptr_alloc_async(
225+
size, stream._h_stream, h_release_context)
217226
if not h_ptr:
218227
HANDLE_RETURN(get_last_error())
219228
raise RuntimeError(

0 commit comments

Comments
 (0)