Skip to content

Commit 6bb6d63

Browse files
Andy-Jostclaude
andcommitted
cuda.core: narrow the memcpy update release note and test the host-operand boundary
Follow-up to #3029. The release note said Graph.update() applies any replaced operand of a captured memcpy node; the driver accepts only a device-to-device replacement in an executable update, and a replacement with host memory takes effect in a new instantiation. A test pins that boundary, and ExecutableMemcpyNode.update() records why it may read the memory types from the definition node. Co-Authored-By: Claude Fable 5.1 <[email protected]>
1 parent 4c893de commit 6bb6d63

3 files changed

Lines changed: 46 additions & 2 deletions

File tree

‎cuda_core/cuda/core/graph/_subclasses.pyx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,6 +1594,10 @@ cdef class ExecutableMemcpyNode(ExecutableGraphNode):
15941594
c_dst, c_src, size, &params.memcpy.copyParams,
15951595
&dst_type, &src_type)
15961596
# A destroyed node is reported by _set_executable_node_params below.
1597+
# The memory types are read from the definition node, not from the
1598+
# instantiated graph. That is correct only while MemcpyNode.update()
1599+
# keeps a unified operand unified, which it does; an executable update
1600+
# rejects a change of memory type.
15971601
if node != NULL:
15981602
with nogil:
15991603
HANDLE_RETURN(cydriver.cuGraphMemcpyNodeGetParams(

‎cuda_core/docs/source/release/1.3.0-notes.rst‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ Fixes and enhancements
264264
addresses, and ``update`` rejected that descriptor with a message about
265265
multidimensional copies. Such an operand stays unified when it is replaced,
266266
because the driver does not accept a change of an operand's memory type in
267-
an instantiated graph, so :meth:`~graph.Graph.update` applies the change as
268-
well. The node's ``repr`` now shows ``U`` for unified operands.
267+
an instantiated graph. Replacing a device operand with another device
268+
buffer can therefore also be applied with :meth:`~graph.Graph.update`;
269+
replacing it with host memory takes effect in a new instantiation only,
270+
because the driver does not let an executable update move an operand to
271+
another device or to the host. The node's ``repr`` now shows ``U`` for
272+
unified operands.
269273
(`#2649 <https://github.com/NVIDIA/cuda-python/issues/2649>`__)

‎cuda_core/tests/graph/test_graph_node_update.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,7 @@ def test_memcpy_update_captured_node_size(init_cuda):
787787
pytest.skip("individual graph node updates require CUDA 12.2+")
788788

789789
stream = init_cuda.create_stream()
790+
# The builder is unused; it keeps the captured graph alive.
790791
builder, graph_def, node, dst, src = _capture_device_memcpy(init_cuda, stream)
791792
recorded = handle_return(driver.cuGraphMemcpyNodeGetParams(node.handle))
792793
assert node.size == 64
@@ -818,6 +819,7 @@ def test_memcpy_update_captured_node_operand(init_cuda, operand):
818819
pytest.skip("individual graph node updates require CUDA 12.2+")
819820

820821
stream = init_cuda.create_stream()
822+
# The builder is unused; it keeps the captured graph alive.
821823
builder, graph_def, node, dst, src = _capture_device_memcpy(init_cuda, stream)
822824
recorded = handle_return(driver.cuGraphMemcpyNodeGetParams(node.handle))
823825
instantiated_before = graph_def.instantiate()
@@ -863,6 +865,7 @@ def test_executable_memcpy_update_on_captured_node(init_cuda):
863865
pytest.skip("individual graph node updates require CUDA 12.2+")
864866

865867
stream = init_cuda.create_stream()
868+
# The builder is unused; it keeps the captured graph alive.
866869
builder, graph_def, node, dst, src = _capture_device_memcpy(init_cuda, stream)
867870
new_src = init_cuda.memory_resource.allocate(64, stream=stream)
868871
new_dst = init_cuda.memory_resource.allocate(64, stream=stream)
@@ -878,6 +881,39 @@ def test_executable_memcpy_update_on_captured_node(init_cuda):
878881
assert _read_device_bytes(dst, stream) == [0] * 64
879882

880883

884+
@pytest.mark.agent_authored(model="claude-fable-5-1")
885+
def test_memcpy_update_captured_node_host_operand(init_cuda):
886+
"""Replacing a captured node's device operand with host memory needs a new instantiation.
887+
888+
The definition node accepts the host buffer and a fresh instantiation
889+
copies from it. An executable update does not: the driver does not let a
890+
memcpy operand move to another device or to the host, so both
891+
:meth:`Graph.update` and the executable view raise. This is a driver
892+
restriction, not something cuda.core enforces.
893+
"""
894+
if driver_version() < (12, 2, 0):
895+
pytest.skip("individual graph node updates require CUDA 12.2+")
896+
897+
stream = init_cuda.create_stream()
898+
# The builder is unused; it keeps the captured graph alive.
899+
builder, graph_def, node, dst, src = _capture_device_memcpy(init_cuda, stream)
900+
instantiated_before = graph_def.instantiate()
901+
host_src = LegacyPinnedMemoryResource().allocate(64)
902+
ctypes.memset(int(host_src.handle), 0xA5, 64)
903+
904+
node.update(src=host_src)
905+
assert node.src == int(host_src.handle)
906+
907+
fresh = graph_def.instantiate()
908+
fresh.launch(stream)
909+
assert _read_device_bytes(dst, stream) == [0xA5] * 64
910+
911+
with pytest.raises(CUDAError, match="PARAMETERS_CHANGED"):
912+
instantiated_before.update(graph_def)
913+
with pytest.raises(CUDAError):
914+
instantiated_before[node].update(dst=dst, src=host_src, size=64)
915+
916+
881917
@pytest.mark.agent_authored(model="gpt-5.6")
882918
def test_definition_node_update_changes_future_instantiations(
883919
definition_update_case,

0 commit comments

Comments
 (0)