Skip to content

Commit 10268ff

Browse files
committed
cuda.core: add explicit dst/src_owner for graph memcpy/memset
Keyword-only *_owner args retain arbitrary objects for raw pointer operands; Buffer+owner combinations are rejected. Strengthen owner tests with weakref retention checks and add src_owner rejection test.
1 parent 813058d commit 10268ff

3 files changed

Lines changed: 222 additions & 34 deletions

File tree

cuda_core/cuda/core/graph/_graph_definition.pyx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ if TYPE_CHECKING:
3333
from cuda.core._device import Device
3434
from cuda.core._event import Event
3535
from cuda.core._launch_config import LaunchConfig
36+
from cuda.core._memory._buffer import Buffer
3637
from cuda.core._module import Kernel
3738
from cuda.core.graph._graph_builder import (
3839
Graph,
@@ -156,17 +157,21 @@ cdef class GraphDefinition:
156157

157158
def memset(
158159
self,
159-
dst: int,
160+
dst: Buffer | int,
160161
value,
161162
size_t width,
163+
*,
162164
size_t height=1,
163-
size_t pitch=0
165+
size_t pitch=0,
166+
dst_owner=None,
164167
) -> MemsetNode:
165168
"""Add an entry-point memset node (no dependencies).
166169

167170
See :meth:`GraphNode.memset` for full documentation.
168171
"""
169-
return self._entry.memset(dst, value, width, height, pitch)
172+
return self._entry.memset(
173+
dst, value, width, height=height, pitch=pitch, dst_owner=dst_owner
174+
)
170175

171176
def launch(self, config: LaunchConfig, kernel: Kernel, *args) -> KernelNode:
172177
"""Add an entry-point kernel launch node (no dependencies).
@@ -200,12 +205,12 @@ cdef class GraphDefinition:
200205
"""
201206
return self._entry.join(*nodes)
202207

203-
def memcpy(self, dst: int, src: int, size_t size) -> MemcpyNode:
208+
def memcpy(self, dst: Buffer | int, src: Buffer | int, size_t size, *, dst_owner=None, src_owner=None) -> MemcpyNode:
204209
"""Add an entry-point memcpy node (no dependencies).
205210

206211
See :meth:`GraphNode.memcpy` for full documentation.
207212
"""
208-
return self._entry.memcpy(dst, src, size)
213+
return self._entry.memcpy(dst, src, size, dst_owner=dst_owner, src_owner=src_owner)
209214

210215
def embed(self, child: GraphDefinition) -> ChildGraphNode:
211216
"""Add an entry-point child graph node (no dependencies).

cuda_core/cuda/core/graph/_graph_node.pyx

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -284,15 +284,25 @@ cdef class GraphNode:
284284
"""
285285
return GN_free(self, <cydriver.CUdeviceptr>dptr)
286286
287-
def memset(self, dst: Buffer | int, value, size_t width, size_t height=1, size_t pitch=0) -> MemsetNode:
287+
def memset(
288+
self,
289+
dst: Buffer | int,
290+
value,
291+
size_t width,
292+
*,
293+
size_t height=1,
294+
size_t pitch=0,
295+
dst_owner=None,
296+
) -> MemsetNode:
288297
"""Add a memset node depending on this node.
289298

290299
Parameters
291300
----------
292301
dst : Buffer or int
293-
Destination. If a :class:`Buffer` is given it is retained for the
294-
graph's lifetime; if a raw device pointer (int) is given it is used
295-
as-is and the caller must keep the underlying memory alive.
302+
Destination. A :class:`Buffer` is retained for the graph's lifetime.
303+
A raw pointer (``int``) is used as-is; the caller must keep the
304+
underlying memory alive, or supply ``dst_owner`` to have the graph
305+
retain it.
296306
value : int or buffer-protocol object
297307
Fill value. int for 1-byte fill (range [0, 256)),
298308
or buffer-protocol object of 1, 2, or 4 bytes.
@@ -302,20 +312,37 @@ cdef class GraphNode:
302312
Number of rows (default 1).
303313
pitch : int, optional
304314
Pitch of destination in bytes (default 0, unused if height is 1).
315+
dst_owner : object, optional
316+
Object retained for the graph's lifetime when ``dst`` is a raw
317+
pointer. Must not be passed when ``dst`` is a :class:`Buffer`, which
318+
is retained automatically.
305319

306320
Returns
307321
-------
308322
MemsetNode
309323
A new MemsetNode representing the memset operation.
324+
325+
Raises
326+
------
327+
ValueError
328+
If ``dst_owner`` is given together with a :class:`Buffer` ``dst``.
310329
"""
311330
cdef cydriver.CUdeviceptr c_dst
312331
cdef unsigned int val
313332
cdef unsigned int elem_size
314-
dst_owner = _resolve_ptr(dst, &c_dst)
333+
dst_keepalive = _resolve_memcpy_operand(dst, dst_owner, "dst", &c_dst)
315334
val, elem_size = _parse_fill_value(value)
316-
return GN_memset(self, c_dst, dst_owner, val, elem_size, width, height, pitch)
317-
318-
def memcpy(self, dst: Buffer | int, src: Buffer | int, size_t size) -> MemcpyNode:
335+
return GN_memset(self, c_dst, dst_keepalive, val, elem_size, width, height, pitch)
336+
337+
def memcpy(
338+
self,
339+
dst: Buffer | int,
340+
src: Buffer | int,
341+
size_t size,
342+
*,
343+
dst_owner=None,
344+
src_owner=None,
345+
) -> MemcpyNode:
319346
"""Add a memcpy node depending on this node.
320347

321348
Copies ``size`` bytes from ``src`` to ``dst``. Memory types are
@@ -325,24 +352,40 @@ cdef class GraphNode:
325352
Parameters
326353
----------
327354
dst : Buffer or int
328-
Destination (device or pinned host). If a :class:`Buffer` is given
329-
it is retained for the graph's lifetime; a raw pointer (int) is used
330-
as-is and the caller must keep the underlying memory alive.
355+
Destination (device or pinned host). A :class:`Buffer` is retained
356+
for the graph's lifetime. A raw pointer (``int``) is used as-is; the
357+
caller must keep the underlying memory alive, or supply ``dst_owner``
358+
to have the graph retain it.
331359
src : Buffer or int
332-
Source (device or pinned host). Retained like ``dst`` when a Buffer.
360+
Source (device or pinned host). Same retention rules as ``dst``;
361+
use ``src_owner`` for a raw pointer.
333362
size : int
334363
Number of bytes to copy.
364+
dst_owner : object, optional
365+
Object retained for the graph's lifetime when ``dst`` is a raw
366+
pointer. Must not be passed when ``dst`` is a :class:`Buffer`, which
367+
is retained automatically.
368+
src_owner : object, optional
369+
Object retained for the graph's lifetime when ``src`` is a raw
370+
pointer. Must not be passed when ``src`` is a :class:`Buffer`, which
371+
is retained automatically.
335372

336373
Returns
337374
-------
338375
MemcpyNode
339376
A new MemcpyNode representing the copy operation.
377+
378+
Raises
379+
------
380+
ValueError
381+
If ``dst_owner`` or ``src_owner`` is given together with a
382+
:class:`Buffer` ``dst`` or ``src`` respectively.
340383
"""
341384
cdef cydriver.CUdeviceptr c_dst
342385
cdef cydriver.CUdeviceptr c_src
343-
dst_owner = _resolve_ptr(dst, &c_dst)
344-
src_owner = _resolve_ptr(src, &c_src)
345-
return GN_memcpy(self, c_dst, dst_owner, c_src, src_owner, size)
386+
dst_keepalive = _resolve_memcpy_operand(dst, dst_owner, "dst", &c_dst)
387+
src_keepalive = _resolve_memcpy_operand(src, src_owner, "src", &c_src)
388+
return GN_memcpy(self, c_dst, dst_keepalive, c_src, src_keepalive, size)
346389
347390
def embed(self, child: GraphDefinition) -> ChildGraphNode:
348391
"""Add a child graph node depending on this node.
@@ -789,20 +832,28 @@ cdef inline FreeNode GN_free(GraphNode self, cydriver.CUdeviceptr c_dptr):
789832
return _registered(FreeNode._create_with_params(create_graph_node_handle(new_node, h_graph), c_dptr))
790833
791834
792-
cdef inline object _resolve_ptr(object value, cydriver.CUdeviceptr* out_ptr):
793-
"""Resolve a memcpy/memset operand into a device pointer.
835+
cdef inline object _resolve_memcpy_operand(
836+
object operand, object owner, str side, cydriver.CUdeviceptr* out_ptr):
837+
"""Resolve a memcpy/memset operand to a device pointer and an owner.
838+
839+
``operand`` is a :class:`Buffer` or a raw integer address; its device
840+
pointer is written to ``out_ptr``. Returns the object to retain on the
841+
graph's slot table for the graph's lifetime: the Buffer itself, or
842+
``owner`` (possibly ``None``) for a raw pointer. ``side`` is ``"dst"`` or
843+
``"src"`` and is used only to compose the error message.
794844

795-
``value`` is a :class:`Buffer` or a raw integer address. For a Buffer the
796-
device pointer is read from the buffer and the buffer is returned so the
797-
caller can retain it on the node's slot table for the graph's lifetime. For
798-
a raw integer no owner is returned and the caller is responsible for keeping
799-
the underlying memory alive.
845+
Raises ValueError if ``operand`` is a Buffer and ``owner`` is not None,
846+
since a Buffer is already retained automatically.
800847
"""
801-
if isinstance(value, Buffer):
802-
out_ptr[0] = as_cu((<Buffer>value)._h_ptr)
803-
return value
804-
out_ptr[0] = <cydriver.CUdeviceptr><uintptr_t>value
805-
return None
848+
if isinstance(operand, Buffer):
849+
if owner is not None:
850+
raise ValueError(
851+
f"{side}_owner cannot be used when {side} is a Buffer"
852+
)
853+
out_ptr[0] = as_cu((<Buffer>operand)._h_ptr)
854+
return operand
855+
out_ptr[0] = <cydriver.CUdeviceptr><uintptr_t>operand
856+
return owner
806857
807858
808859
cdef inline MemsetNode GN_memset(
@@ -837,7 +888,7 @@ cdef inline MemsetNode GN_memset(
837888
&new_node, as_cu(h_graph), deps, num_deps,
838889
&memset_params, ctx))
839890
840-
# Retain a Buffer destination for the graph's lifetime (slot 0).
891+
# Retain the destination owner for the graph's lifetime (slot 0).
841892
if dst_owner is not None:
842893
HANDLE_RETURN(graph_set_slot(h_graph, new_node, 0, make_opaque_py(dst_owner)))
843894
@@ -902,7 +953,7 @@ cdef inline MemcpyNode GN_memcpy(
902953
HANDLE_RETURN(cydriver.cuGraphAddMemcpyNode(
903954
&new_node, as_cu(h_graph), deps, num_deps, &params, ctx))
904955
905-
# Retain Buffer operands for the graph's lifetime (dst -> slot 0, src -> slot 1).
956+
# Retain operand owners for the graph's lifetime (dst -> slot 0, src -> slot 1).
906957
if dst_owner is not None:
907958
HANDLE_RETURN(graph_set_slot(h_graph, new_node, 0, make_opaque_py(dst_owner)))
908959
if src_owner is not None:

cuda_core/tests/graph/test_graph_definition_lifetime.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,3 +695,135 @@ def test_memcpy_buffers_survive_graph_clone(init_cuda):
695695
out = (ctypes.c_uint8 * 4)(0)
696696
handle_return(driver.cuMemcpyDtoH(out, dst_dptr, 4))
697697
assert list(out) == [0xCD] * 4
698+
699+
700+
# =============================================================================
701+
# Explicit dst_owner / src_owner for raw pointer operands
702+
# =============================================================================
703+
704+
705+
def test_memset_raw_ptr_with_dst_owner(init_cuda):
706+
"""Raw dst address plus dst_owner: the graph retains the owner until destroyed."""
707+
from cuda.core._utils.cuda_utils import driver, handle_return
708+
709+
_skip_if_no_mempool()
710+
dev = Device()
711+
mr = DeviceMemoryResource(dev)
712+
buf = mr.allocate(4, stream=dev.default_stream)
713+
dev.default_stream.sync()
714+
buf_weak = weakref.ref(buf)
715+
dptr = int(buf.handle)
716+
717+
g = GraphDefinition()
718+
g.memset(dptr, 0xAB, 4, dst_owner=buf)
719+
720+
del buf
721+
gc.collect()
722+
assert buf_weak() is not None # graph retains the explicit owner
723+
724+
stream = dev.create_stream()
725+
g.instantiate().launch(stream)
726+
stream.sync()
727+
728+
out = (ctypes.c_uint8 * 4)(0)
729+
handle_return(driver.cuMemcpyDtoH(out, dptr, 4))
730+
assert list(out) == [0xAB] * 4
731+
732+
del g
733+
_wait_until(lambda: buf_weak() is None)
734+
735+
736+
def test_memcpy_raw_ptrs_with_owners(init_cuda):
737+
"""Raw src/dst addresses: the graph retains both owners until destroyed."""
738+
from cuda.core._utils.cuda_utils import driver, handle_return
739+
740+
_skip_if_no_mempool()
741+
dev = Device()
742+
mr = DeviceMemoryResource(dev)
743+
src = mr.allocate(4, stream=dev.default_stream)
744+
dst = mr.allocate(4, stream=dev.default_stream)
745+
src.fill(0xCD, stream=dev.default_stream)
746+
dev.default_stream.sync()
747+
src_weak = weakref.ref(src)
748+
dst_weak = weakref.ref(dst)
749+
src_dptr = int(src.handle)
750+
dst_dptr = int(dst.handle)
751+
752+
g = GraphDefinition()
753+
g.memcpy(dst_dptr, src_dptr, 4, dst_owner=dst, src_owner=src)
754+
755+
del src, dst
756+
gc.collect()
757+
assert src_weak() is not None and dst_weak() is not None # both owners retained
758+
759+
stream = dev.create_stream()
760+
g.instantiate().launch(stream)
761+
stream.sync()
762+
763+
out = (ctypes.c_uint8 * 4)(0)
764+
handle_return(driver.cuMemcpyDtoH(out, dst_dptr, 4))
765+
assert list(out) == [0xCD] * 4
766+
767+
del g
768+
_wait_until(lambda: src_weak() is None and dst_weak() is None)
769+
770+
771+
def test_memcpy_mixed_buffer_and_raw_owner(init_cuda):
772+
"""Buffer dst is auto-retained; raw src uses src_owner. Both survive until destroyed."""
773+
from cuda.core._utils.cuda_utils import driver, handle_return
774+
775+
_skip_if_no_mempool()
776+
dev = Device()
777+
mr = DeviceMemoryResource(dev)
778+
src = mr.allocate(4, stream=dev.default_stream)
779+
dst = mr.allocate(4, stream=dev.default_stream)
780+
src.fill(0xCD, stream=dev.default_stream)
781+
dev.default_stream.sync()
782+
src_weak = weakref.ref(src)
783+
dst_weak = weakref.ref(dst)
784+
src_dptr = int(src.handle)
785+
dst_dptr = int(dst.handle)
786+
787+
g = GraphDefinition()
788+
g.memcpy(dst, src_dptr, 4, src_owner=src)
789+
790+
del src, dst
791+
gc.collect()
792+
assert src_weak() is not None and dst_weak() is not None # explicit + auto owner
793+
794+
stream = dev.create_stream()
795+
g.instantiate().launch(stream)
796+
stream.sync()
797+
798+
out = (ctypes.c_uint8 * 4)(0)
799+
handle_return(driver.cuMemcpyDtoH(out, dst_dptr, 4))
800+
assert list(out) == [0xCD] * 4
801+
802+
del g
803+
_wait_until(lambda: src_weak() is None and dst_weak() is None)
804+
805+
806+
def test_memcpy_buffer_and_dst_owner_rejected(init_cuda):
807+
"""dst_owner cannot be combined with a Buffer dst operand."""
808+
_skip_if_no_mempool()
809+
dev = Device()
810+
mr = DeviceMemoryResource(dev)
811+
buf = mr.allocate(4, stream=dev.default_stream)
812+
dev.default_stream.sync()
813+
814+
g = GraphDefinition()
815+
with pytest.raises(ValueError, match="dst_owner cannot be used when dst is a Buffer"):
816+
g.memcpy(buf, buf, 4, dst_owner=object())
817+
818+
819+
def test_memcpy_buffer_and_src_owner_rejected(init_cuda):
820+
"""src_owner cannot be combined with a Buffer src operand."""
821+
_skip_if_no_mempool()
822+
dev = Device()
823+
mr = DeviceMemoryResource(dev)
824+
buf = mr.allocate(4, stream=dev.default_stream)
825+
dev.default_stream.sync()
826+
827+
g = GraphDefinition()
828+
with pytest.raises(ValueError, match="src_owner cannot be used when src is a Buffer"):
829+
g.memcpy(buf, buf, 4, src_owner=object())

0 commit comments

Comments
 (0)