@@ -213,7 +213,9 @@ cdef class Buffer:
213213 cdef uintptr_t c_ptr = < uintptr_t> (int (ptr))
214214 cdef Stream s
215215 if mr is not None:
216- s = Stream_accept(default_stream() if stream is None else stream)
216+ # Validate before taking ownership so a bad stream does not cause
217+ # construction failure to deallocate the caller's pointer.
218+ s = default_stream() if stream is None else Stream_accept(stream)
217219 _require_deallocation_stream_context(s )
218220 self._h_ptr = deviceptr_create_with_mr(c_ptr, size, mr)
219221 HANDLE_RETURN(set_deallocation_stream(self._h_ptr , s._h_stream ))
@@ -238,7 +240,7 @@ cdef class Buffer:
238240 # The parent process's stream is not portable across processes, so the
239241 # pickle path cannot thread an explicit stream through. Seed the
240242 # imported buffer's deallocation with the current context's default
241- # stream; the receiver can override via buffer. close(stream) .
243+ # stream; the receiver can override it before or during close.
242244 return Buffer.from_ipc_descriptor(mr, ipc_descriptor, stream = default_stream())
243245
244246 def __reduce__ (self ) -> tuple[object , ...]:
@@ -333,9 +335,45 @@ cdef class Buffer:
333335 stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`, optional
334336 The stream object to use for asynchronous deallocation. If None ,
335337 the deallocation stream stored in the handle is used.
338+
339+ See Also
340+ --------
341+ set_deallocation_stream
342+ Change the deallocation stream without closing the buffer.
336343 """
337344 Buffer_close(self , stream )
338345
346+ def set_deallocation_stream(self , stream: Stream | GraphBuilder ) -> None:
347+ """Change the stream that orders this buffer's eventual deallocation.
348+
349+ The buffer remains open and usable. A later :meth:`close` without a
350+ stream , garbage collection , or release of the final retained device
351+ pointer handle uses the replacement stream.
352+
353+ This method does not synchronize streams or establish dependencies.
354+ The caller must ensure that allocation and all accesses are ordered
355+ before the deallocation on ``stream``.
356+
357+ Parameters
358+ ----------
359+ stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`
360+ The stream to use for eventual asynchronous deallocation.
361+
362+ Raises
363+ ------
364+ RuntimeError
365+ If the buffer is already closed , or if a default-stream token
366+ cannot be bound because no CUDA context is current.
367+ TypeError
368+ If ``stream`` is ``None`` or is not an accepted stream object.
369+
370+ Notes
371+ -----
372+ Synchronizing concurrent mutation and destruction of the same buffer
373+ is the caller's responsibility.
374+ """
375+ Buffer_set_deallocation_stream(self , stream )
376+
339377 def __enter__(self ):
340378 return self
341379
@@ -667,16 +705,23 @@ cdef Buffer Buffer_from_deviceptr_handle(
667705 return buf
668706
669707
708+ cdef inline void Buffer_set_deallocation_stream(Buffer self , object stream):
709+ """ Validate and replace a live buffer's deallocation recipe."""
710+ cdef Stream s
711+ if not self ._h_ptr:
712+ raise RuntimeError (" Cannot set the deallocation stream on a closed Buffer" )
713+ s = Stream_accept(stream)
714+ _require_deallocation_stream_context(s)
715+ HANDLE_RETURN(set_deallocation_stream(self ._h_ptr, s._h_stream))
716+
717+
670718cdef inline void Buffer_close(Buffer self , object stream):
671719 """ Close a buffer, freeing its memory."""
672- cdef Stream s
673720 if not self ._h_ptr:
674721 return
675722 # Update deallocation stream if provided
676723 if stream is not None :
677- s = Stream_accept(stream)
678- _require_deallocation_stream_context(s)
679- HANDLE_RETURN(set_deallocation_stream(self ._h_ptr, s._h_stream))
724+ Buffer_set_deallocation_stream(self , stream)
680725 # Reset handle - RAII deleter will free the memory (and release owner ref in C++)
681726 self ._h_ptr.reset()
682727 self ._size = 0
0 commit comments