# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """Tests for GraphDefinition topology, node types, instantiation, and execution.""" import ctypes import gc import sys import weakref from collections.abc import Callable from dataclasses import dataclass, field import pytest from helpers.graph_kernels import compile_common_kernels from helpers.memory import skip_if_managed_mempool_unsupported, skip_if_mempool_unsupported from helpers.misc import try_create_condition from cuda.core import Device, LaunchConfig from cuda.core.graph import ( AllocNode, ChildGraphNode, ConditionalNode, EmptyNode, EventRecordNode, EventWaitNode, FreeNode, GraphCompleteOptions, GraphDebugPrintOptions, GraphDefinition, GraphNode, HostCallbackNode, IfElseNode, IfNode, KernelNode, MemcpyNode, MemsetNode, SwitchNode, WhileNode, ) from cuda.core.typing import GraphConditionalType, GraphMemoryType ALLOC_SIZE = 1024 def _has_node_get_params(): from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version return BUILD_CUDA_MAJOR >= 13 and driver_version() >= (13, 2, 0) _HAS_NODE_GET_PARAMS = _has_node_get_params() def _bindings_major_version(): from cuda.core._utils.version import BUILD_CUDA_MAJOR return BUILD_CUDA_MAJOR _BINDINGS_MAJOR = _bindings_major_version() # ============================================================================= # GraphSpec — representative graph topologies # ============================================================================= @dataclass class GraphSpec: """Describes a graph topology with expected structural properties.""" name: str graph_definition: GraphDefinition named_nodes: dict = field(default_factory=dict) expected_edges: set = field(default_factory=set) expected_pred: dict = field(default_factory=dict) expected_succ: dict = field(default_factory=dict) def _build_empty(): """No nodes, no edges.""" return GraphSpec("empty", GraphDefinition()) def _build_single(): """One alloc node, no edges.""" g = GraphDefinition() a = g.allocate(ALLOC_SIZE) return GraphSpec( "single", g, named_nodes={"a": a}, expected_edges=set(), expected_pred={"a": set()}, expected_succ={"a": set()}, ) def _build_chain(): """Linear chain: a -> b -> c.""" g = GraphDefinition() a = g.allocate(ALLOC_SIZE) b = a.allocate(ALLOC_SIZE) c = b.allocate(ALLOC_SIZE) return GraphSpec( "chain", g, named_nodes={"a": a, "b": b, "c": c}, expected_edges={("a", "b"), ("b", "c")}, expected_pred={"a": set(), "b": {"a"}, "c": {"b"}}, expected_succ={"a": {"b"}, "b": {"c"}, "c": set()}, ) def _build_fan_out(): """One node feeds three: a -> {b, c, d}.""" g = GraphDefinition() a = g.allocate(ALLOC_SIZE) b = a.allocate(ALLOC_SIZE) c = a.allocate(ALLOC_SIZE) d = a.allocate(ALLOC_SIZE) return GraphSpec( "fan_out", g, named_nodes={"a": a, "b": b, "c": c, "d": d}, expected_edges={("a", "b"), ("a", "c"), ("a", "d")}, expected_pred={"a": set(), "b": {"a"}, "c": {"a"}, "d": {"a"}}, expected_succ={"a": {"b", "c", "d"}, "b": set(), "c": set(), "d": set()}, ) def _build_fan_in(): """Three entry nodes merge: {a, b, c} -> d (join).""" g = GraphDefinition() a = g.allocate(ALLOC_SIZE) b = g.allocate(ALLOC_SIZE) c = g.allocate(ALLOC_SIZE) d = g.join(a, b, c) return GraphSpec( "fan_in", g, named_nodes={"a": a, "b": b, "c": c, "d": d}, expected_edges={("a", "d"), ("b", "d"), ("c", "d")}, expected_pred={"a": set(), "b": set(), "c": set(), "d": {"a", "b", "c"}}, expected_succ={"a": {"d"}, "b": {"d"}, "c": {"d"}, "d": set()}, ) def _build_diamond(): """Diamond: a -> {b, c} -> d (join).""" g = GraphDefinition() a = g.allocate(ALLOC_SIZE) b = a.allocate(ALLOC_SIZE) c = a.allocate(ALLOC_SIZE) d = b.join(c) return GraphSpec( "diamond", g, named_nodes={"a": a, "b": b, "c": c, "d": d}, expected_edges={("a", "b"), ("a", "c"), ("b", "d"), ("c", "d")}, expected_pred={"a": set(), "b": {"a"}, "c": {"a"}, "d": {"b", "c"}}, expected_succ={"a": {"b", "c"}, "b": {"d"}, "c": {"d"}, "d": set()}, ) def _build_disconnected(): """Two independent entry nodes: a, b.""" g = GraphDefinition() a = g.allocate(ALLOC_SIZE) b = g.allocate(ALLOC_SIZE) return GraphSpec( "disconnected", g, named_nodes={"a": a, "b": b}, expected_edges=set(), expected_pred={"a": set(), "b": set()}, expected_succ={"a": set(), "b": set()}, ) _ALL_BUILDERS = [ pytest.param(_build_empty, id="empty"), pytest.param(_build_single, id="single"), pytest.param(_build_chain, id="chain"), pytest.param(_build_fan_out, id="fan_out"), pytest.param(_build_fan_in, id="fan_in"), pytest.param(_build_diamond, id="diamond"), pytest.param(_build_disconnected, id="disconnected"), ] _NONEMPTY_BUILDERS = [p for p in _ALL_BUILDERS if p.values[0] is not _build_empty] @pytest.fixture(params=_ALL_BUILDERS) def graph_spec(request, init_cuda): if request.param is not _build_empty: skip_if_mempool_unsupported() return request.param() @pytest.fixture(params=_NONEMPTY_BUILDERS) def nonempty_graph_spec(request, init_cuda): skip_if_mempool_unsupported() return request.param() # ============================================================================= # NodeSpec — representative node types # ============================================================================= @dataclass class NodeSpec: """Describes a node type with expected properties. The builder returns (node, expected_attrs) where expected_attrs maps property names to expected values. Callable values are treated as predicates (e.g., ``lambda v: v != 0``). """ name: str expected_class: type expected_type_name: str builder: Callable[[GraphDefinition], tuple[GraphNode, dict]] reconstructed_class: type | None = None needs_mempool: bool = True @property def roundtrip_class(self): """Class expected after reconstruction from the driver.""" return self.reconstructed_class or self.expected_class def _build_empty_node(g): a = g.allocate(ALLOC_SIZE) b = g.allocate(ALLOC_SIZE) return g.join(a, b), {} def _build_kernel_node(g): mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=(2, 3, 1), block=(32, 4, 1), shmem_size=128) entry = g.allocate(ALLOC_SIZE) node = entry.launch(config, kernel) return node, { "grid": (2, 3, 1), "block": (32, 4, 1), "shmem_size": 128, "kernel": kernel, "config": config, } def _build_alloc_node(g): device_id = Device().device_id entry = g.allocate(ALLOC_SIZE) node = entry.allocate(ALLOC_SIZE) return node, { "dptr": lambda v: v != 0, "bytesize": ALLOC_SIZE, "device_id": device_id, "memory_type": "device", "peer_access": (), } def _build_alloc_managed_node(g): skip_if_managed_mempool_unsupported() device_id = Device().device_id entry = g.allocate(ALLOC_SIZE) node = entry.allocate(ALLOC_SIZE, memory_type=GraphMemoryType.MANAGED) return node, { "dptr": lambda v: v != 0, "bytesize": ALLOC_SIZE, "device_id": device_id, "memory_type": "managed", "peer_access": (), } def _build_free_node(g): alloc = g.allocate(ALLOC_SIZE) node = alloc.deallocate(alloc.dptr) return node, { "dptr": alloc.dptr, } def _build_memset_node(g): alloc = g.allocate(ALLOC_SIZE) node = alloc.memset(alloc.dptr, 42, ALLOC_SIZE) return node, { "dptr": alloc.dptr, "value": 42, "element_size": 1, "width": ALLOC_SIZE, "height": 1, "pitch": 0, } def _build_memset_node_u16(g): alloc = g.allocate(ALLOC_SIZE) node = alloc.memset(alloc.dptr, b"\xab\xcd", ALLOC_SIZE // 2) return node, { "dptr": alloc.dptr, "value": int.from_bytes(b"\xab\xcd", byteorder="little"), "element_size": 2, "width": ALLOC_SIZE // 2, "height": 1, "pitch": 0, } def _build_memset_node_u32(g): alloc = g.allocate(ALLOC_SIZE) node = alloc.memset(alloc.dptr, b"\x01\x02\x03\x04", ALLOC_SIZE // 4) return node, { "dptr": alloc.dptr, "value": int.from_bytes(b"\x01\x02\x03\x04", byteorder="little"), "element_size": 4, "width": ALLOC_SIZE // 4, "height": 1, "pitch": 0, } def _build_memset_node_2d(g): rows = 4 cols = ALLOC_SIZE // rows alloc = g.allocate(ALLOC_SIZE) node = alloc.memset(alloc.dptr, 0xFF, cols, height=rows, pitch=cols) return node, { "dptr": alloc.dptr, "value": 0xFF, "element_size": 1, "width": cols, "height": rows, "pitch": cols, } def _build_event_record_node(g): event = Device().create_event() entry = g.allocate(ALLOC_SIZE) node = entry.record(event) return node, { "event": event, } def _build_event_wait_node(g): event = Device().create_event() entry = g.allocate(ALLOC_SIZE) node = entry.wait(event) return node, { "event": event, } def _build_memcpy_node(g): src_alloc = g.allocate(ALLOC_SIZE) dst_alloc = g.allocate(ALLOC_SIZE) dep = g.join(src_alloc, dst_alloc) node = dep.memcpy(dst_alloc.dptr, src_alloc.dptr, ALLOC_SIZE) return node, { "dst": dst_alloc.dptr, "src": src_alloc.dptr, "size": ALLOC_SIZE, } def _build_host_callback_node(g): def my_callback(): pass node = g.callback(my_callback) return node, { "callback": lambda v: v is my_callback, } def _build_host_callback_cfunc_node(g): import ctypes CALLBACK = ctypes.CFUNCTYPE(None, ctypes.c_void_p) @CALLBACK def noop(data): pass node = g.callback(noop) return node, {} def _build_child_graph_node(g): child = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) child.launch(config, kernel) child.launch(config, kernel) node = g.embed(child) return node, { "child_graph": lambda v: isinstance(v, GraphDefinition) and len(v.nodes()) == 2, } def _build_if_then_node(g): condition = try_create_condition(g) node = g.if_then(condition) return node, { "condition": condition, "cond_type": lambda v: isinstance(v, GraphConditionalType) and v == "if", "branches": lambda v: isinstance(v, tuple) and len(v) == 1, "then": lambda v: isinstance(v, GraphDefinition), } def _build_if_else_node(g): condition = try_create_condition(g) node = g.if_else(condition) return node, { "condition": condition, "cond_type": lambda v: isinstance(v, GraphConditionalType) and v == "if", "branches": lambda v: isinstance(v, tuple) and len(v) == 2, "then": lambda v: isinstance(v, GraphDefinition), "else_": lambda v: isinstance(v, GraphDefinition), } def _build_while_loop_node(g): condition = try_create_condition(g) node = g.while_loop(condition) return node, { "condition": condition, "cond_type": lambda v: isinstance(v, GraphConditionalType) and v == "while", "branches": lambda v: isinstance(v, tuple) and len(v) == 1, "body": lambda v: isinstance(v, GraphDefinition), } def _build_switch_node(g): condition = try_create_condition(g) node = g.switch(condition, 3) return node, { "condition": condition, "cond_type": lambda v: isinstance(v, GraphConditionalType) and v == "switch", "branches": lambda v: isinstance(v, tuple) and len(v) == 3, } _NODE_SPECS = [ pytest.param(NodeSpec("empty", EmptyNode, "CU_GRAPH_NODE_TYPE_EMPTY", _build_empty_node), id="empty"), pytest.param(NodeSpec("kernel", KernelNode, "CU_GRAPH_NODE_TYPE_KERNEL", _build_kernel_node), id="kernel"), pytest.param(NodeSpec("alloc", AllocNode, "CU_GRAPH_NODE_TYPE_MEM_ALLOC", _build_alloc_node), id="alloc"), pytest.param( NodeSpec("alloc_managed", AllocNode, "CU_GRAPH_NODE_TYPE_MEM_ALLOC", _build_alloc_managed_node), id="alloc_managed", marks=pytest.mark.skipif(_BINDINGS_MAJOR < 13, reason="managed alloc requires the CUDA 13 build"), ), pytest.param(NodeSpec("free", FreeNode, "CU_GRAPH_NODE_TYPE_MEM_FREE", _build_free_node), id="free"), pytest.param(NodeSpec("memset", MemsetNode, "CU_GRAPH_NODE_TYPE_MEMSET", _build_memset_node), id="memset"), pytest.param( NodeSpec("memset_u16", MemsetNode, "CU_GRAPH_NODE_TYPE_MEMSET", _build_memset_node_u16), id="memset_u16" ), pytest.param( NodeSpec("memset_u32", MemsetNode, "CU_GRAPH_NODE_TYPE_MEMSET", _build_memset_node_u32), id="memset_u32" ), pytest.param(NodeSpec("memset_2d", MemsetNode, "CU_GRAPH_NODE_TYPE_MEMSET", _build_memset_node_2d), id="memset_2d"), pytest.param( NodeSpec("memcpy", MemcpyNode, "CU_GRAPH_NODE_TYPE_MEMCPY", _build_memcpy_node), id="memcpy", ), pytest.param( NodeSpec( "child_graph", ChildGraphNode, "CU_GRAPH_NODE_TYPE_GRAPH", _build_child_graph_node, needs_mempool=False ), id="child_graph", ), pytest.param( NodeSpec( "host_callback", HostCallbackNode, "CU_GRAPH_NODE_TYPE_HOST", _build_host_callback_node, needs_mempool=False ), id="host_callback", ), pytest.param( NodeSpec( "host_callback_cfunc", HostCallbackNode, "CU_GRAPH_NODE_TYPE_HOST", _build_host_callback_cfunc_node, needs_mempool=False, ), id="host_callback_cfunc", ), pytest.param( NodeSpec("event_record", EventRecordNode, "CU_GRAPH_NODE_TYPE_EVENT_RECORD", _build_event_record_node), id="event_record", ), pytest.param( NodeSpec("event_wait", EventWaitNode, "CU_GRAPH_NODE_TYPE_WAIT_EVENT", _build_event_wait_node), id="event_wait", ), pytest.param( NodeSpec( "if_then", IfNode, "CU_GRAPH_NODE_TYPE_CONDITIONAL", _build_if_then_node, reconstructed_class=IfNode if _HAS_NODE_GET_PARAMS else ConditionalNode, needs_mempool=False, ), id="if_then", ), pytest.param( NodeSpec( "if_else", IfElseNode, "CU_GRAPH_NODE_TYPE_CONDITIONAL", _build_if_else_node, reconstructed_class=IfElseNode if _HAS_NODE_GET_PARAMS else ConditionalNode, needs_mempool=False, ), id="if_else", ), pytest.param( NodeSpec( "while_loop", WhileNode, "CU_GRAPH_NODE_TYPE_CONDITIONAL", _build_while_loop_node, reconstructed_class=WhileNode if _HAS_NODE_GET_PARAMS else ConditionalNode, needs_mempool=False, ), id="while_loop", ), pytest.param( NodeSpec( "switch", SwitchNode, "CU_GRAPH_NODE_TYPE_CONDITIONAL", _build_switch_node, reconstructed_class=SwitchNode if _HAS_NODE_GET_PARAMS else ConditionalNode, needs_mempool=False, ), id="switch", ), ] @pytest.fixture(params=_NODE_SPECS) def node_spec(request, init_cuda): spec = request.param if spec.needs_mempool: skip_if_mempool_unsupported() g = GraphDefinition() node, expected_attrs = spec.builder(g) return spec, g, node, expected_attrs # ============================================================================= # Fixtures # ============================================================================= # ============================================================================= # Topology tests (parameterized over graph specs) # ============================================================================= def test_node_count(graph_spec): """Graph contains the expected number of nodes.""" assert len(graph_spec.graph_definition.nodes()) == len(graph_spec.named_nodes) def test_nodes_match(nonempty_graph_spec): """nodes() returns exactly the expected nodes.""" spec = nonempty_graph_spec assert set(spec.graph_definition.nodes()) == set(spec.named_nodes.values()) def test_edges(graph_spec): """edges() returns exactly the expected edges.""" spec = graph_spec node_to_name = {v: k for k, v in spec.named_nodes.items()} actual = {(node_to_name[a], node_to_name[b]) for a, b in spec.graph_definition.edges()} assert actual == spec.expected_edges def test_pred(nonempty_graph_spec): """Each node has the expected predecessors.""" spec = nonempty_graph_spec node_to_name = {v: k for k, v in spec.named_nodes.items()} for name, node in spec.named_nodes.items(): actual = {node_to_name[p] for p in node.pred} assert actual == spec.expected_pred[name], f"pred mismatch for node {name}" def test_succ(nonempty_graph_spec): """Each node has the expected successors.""" spec = nonempty_graph_spec node_to_name = {v: k for k, v in spec.named_nodes.items()} for name, node in spec.named_nodes.items(): actual = {node_to_name[s] for s in node.succ} assert actual == spec.expected_succ[name], f"succ mismatch for node {name}" @pytest.mark.parametrize("adjacency_name", ("pred", "succ")) @pytest.mark.agent_authored(model="gpt-5.6") def test_large_adjacency_set_is_not_truncated(init_cuda, adjacency_name): """Adjacency queries return and remove edges beyond the old 16-edge buffer.""" g = GraphDefinition() hub = g.empty() neighbors = [g.empty() for _ in range(20)] adjacency = getattr(hub, adjacency_name) adjacency.update(neighbors) expected_edges = ( {(node, hub) for node in neighbors} if adjacency_name == "pred" else {(hub, node) for node in neighbors} ) assert len(adjacency) == 20 assert set(adjacency) == set(neighbors) assert neighbors[-1] in adjacency assert g.edges() == expected_edges adjacency.clear() assert len(adjacency) == 0 assert g.edges() == set() @pytest.mark.agent_authored(model="gpt-5.6") def test_large_graph_queries_are_not_truncated(init_cuda): """Graph queries return nodes and edges beyond the old 128-item buffers.""" g = GraphDefinition() nodes = [g.empty() for _ in range(130)] nodes[0].succ.update(nodes[1:]) nodes[1].succ.add(nodes[2]) expected_edges = {(nodes[0], node) for node in nodes[1:]} expected_edges.add((nodes[1], nodes[2])) assert g.nodes() == set(nodes) assert g.edges() == expected_edges def test_node_graph_property(nonempty_graph_spec): """Every node's .graph property returns the parent GraphDefinition.""" spec = nonempty_graph_spec for name, node in spec.named_nodes.items(): assert node.graph == spec.graph_definition, f"graph mismatch for node {name}" # ============================================================================= # Node type tests (parameterized over node specs) # ============================================================================= def test_node_isinstance(node_spec): """GraphNode is an instance of the expected subclass.""" spec, g, node, _ = node_spec assert isinstance(node, spec.expected_class) assert isinstance(node, GraphNode) def test_node_type_property(node_spec): """Node.type returns the expected CUgraphNodeType.""" spec, g, node, _ = node_spec assert node.type.name == spec.expected_type_name def test_node_type_preserved_by_nodes(node_spec): """Node type is preserved when retrieved via graph_definition.nodes().""" spec, g, node, _ = node_spec all_nodes = g.nodes() matched = [n for n in all_nodes if n == node] assert len(matched) == 1 assert isinstance(matched[0], spec.roundtrip_class) assert matched[0] is node def test_node_type_preserved_by_pred_succ(node_spec): """Node type is preserved when retrieved via pred/succ traversal.""" spec, g, node, _ = node_spec for predecessor in node.pred: matched = [s for s in predecessor.succ if s == node] assert len(matched) == 1 assert isinstance(matched[0], spec.roundtrip_class) assert matched[0] is node def test_node_attrs(node_spec): """Type-specific attributes have expected values after construction.""" spec, g, node, expected_attrs = node_spec if not expected_attrs: pytest.skip("no type-specific attributes") for attr, expected in expected_attrs.items(): actual = getattr(node, attr) if callable(expected): assert expected(actual), f"{spec.name}.{attr}: check failed (got {actual})" else: assert actual == expected, f"{spec.name}.{attr}: expected {expected}, got {actual}" def test_node_attrs_preserved_by_nodes(node_spec): """Type-specific attributes survive round-trip through graph_definition.nodes().""" spec, g, node, expected_attrs = node_spec if not expected_attrs: pytest.skip("no type-specific attributes") if spec.roundtrip_class != spec.expected_class: pytest.skip("reconstructed type differs — attrs not preserved") retrieved = next(n for n in g.nodes() if n == node) for attr in expected_attrs: assert getattr(retrieved, attr) == getattr(node, attr), f"{spec.name}.{attr} not preserved by nodes()" @pytest.mark.agent_authored(model="claude-opus-4.8") def test_host_callback_node_reconstructed_from_embedded_child(init_cuda): """A host-callback node read through an embedded child graph is reconstructed via _create_from_driver.""" # The same-wrapper nodes() (test_node_attrs_preserved_by_nodes) returns the # registry-cached object and never exercises reconstruction; only the embedded # child graph carries fresh, unregistered node handles. Host callback is # mempool-free so it reconstructs here; alloc-based nodes stay cached. child = GraphDefinition() _build_host_callback_node(child) parent = GraphDefinition() reconstructed = list(parent.embed(child).child_graph.nodes()) assert any(isinstance(n, HostCallbackNode) for n in reconstructed), ( f"no reconstructed HostCallbackNode in {[type(n).__name__ for n in reconstructed]}" ) def test_identity_preservation(init_cuda): """Round-trips through nodes(), edges(), and pred/succ return extant objects rather than duplicates.""" g = GraphDefinition() a = g.empty() b = g.empty() # nodes() assert any(x is a for x in g.nodes()) assert any(x is b for x in g.nodes()) # succ/pred a.succ = {b} (b2,) = a.succ assert b2 is b (a2,) = b.pred assert a2 is a # edges() ((a2, b2),) = g.edges() assert a2 is a assert b2 is b def test_registry_cleanup(init_cuda): """Node registry entries are removed on destroy() and graph teardown.""" import gc from cuda.core.graph._graph_node import _node_registry def registered(node): return any(v is node for v in _node_registry.values()) gc.collect() assert len(_node_registry) == 0 g = GraphDefinition() a = g.empty() b = g.empty() c = g.empty() assert len(_node_registry) == 3 assert registered(a) assert registered(b) assert registered(c) a.destroy() assert len(_node_registry) == 2 assert not registered(a) assert registered(b) assert registered(c) del g gc.collect() assert len(_node_registry) == 2 assert registered(b) assert registered(c) b.destroy() assert len(_node_registry) == 1 assert not registered(b) assert registered(c) del c gc.collect() assert len(_node_registry) == 0 # ============================================================================= # GraphDefinition basics # ============================================================================= def test_graphdef_handle_valid(init_cuda): """GraphDefinition has a valid non-null handle.""" sample_graphdef = GraphDefinition() assert sample_graphdef.handle is not None assert int(sample_graphdef.handle) != 0 def test_graph_identity_attributes(init_cuda): from cuda.core._utils.version import driver_version definition = GraphDefinition() node = definition.empty() other = definition.empty() graph = definition.instantiate() try: if driver_version() < (13, 1, 0): with pytest.raises(NotImplementedError): _ = definition.id with pytest.raises(NotImplementedError): _ = graph.id return assert isinstance(definition.id, int) assert isinstance(graph.id, int) assert node.graph == definition assert node.local_id != other.local_id assert node.tools_id != other.tools_id finally: graph.close() def test_graphdef_entry_is_virtual(init_cuda): """Internal entry node is virtual (no pred/succ, type is None).""" sample_graphdef = GraphDefinition() entry = sample_graphdef._entry assert isinstance(entry, GraphNode) assert entry.pred == set() assert entry.succ == set() assert entry.type is None # ============================================================================= # Alloc/free API # ============================================================================= def test_alloc_zero_size_fails(init_cuda): """Alloc with zero size raises error (CUDA limitation).""" sample_graphdef = GraphDefinition() skip_if_mempool_unsupported() from cuda.core._utils.cuda_utils import CUDAError with pytest.raises(CUDAError): sample_graphdef.allocate(0) def test_free_creates_dependency(init_cuda): """Free node depends on its predecessor.""" sample_graphdef = GraphDefinition() skip_if_mempool_unsupported() alloc = sample_graphdef.allocate(ALLOC_SIZE) free = alloc.deallocate(alloc.dptr) assert alloc in free.pred def test_alloc_free_chain(init_cuda): """Alloc and free can be chained.""" sample_graphdef = GraphDefinition() skip_if_mempool_unsupported() a1 = sample_graphdef.allocate(ALLOC_SIZE) a2 = a1.allocate(ALLOC_SIZE) f2 = a2.deallocate(a2.dptr) f1 = f2.deallocate(a1.dptr) assert a1 in a2.pred assert a2 in f2.pred assert f2 in f1.pred # ============================================================================= # Allocation options (error cases, input variants, multi-GPU) # ============================================================================= def test_alloc_memory_type_invalid(init_cuda): """Invalid memory type raises ValueError.""" sample_graphdef = GraphDefinition() with pytest.raises(ValueError, match="'invalid' is not a valid GraphMemoryType. Must be "): sample_graphdef.allocate(ALLOC_SIZE, memory_type="invalid") @pytest.mark.parametrize( "device_spec", [ pytest.param(lambda d: d.device_id, id="device_id"), pytest.param(lambda d: d, id="Device_object"), ], ) def test_alloc_device_option(init_cuda, device_spec): """Device can be specified as int or Device object.""" sample_graphdef = GraphDefinition() skip_if_mempool_unsupported() device = Device() node = sample_graphdef.allocate(ALLOC_SIZE, device=device_spec(device)) assert node.dptr != 0 def test_alloc_peer_access(mempool_device_x2): """AllocNode.peer_access reflects requested peers.""" d0, d1 = mempool_device_x2 g = GraphDefinition() node = g.allocate(ALLOC_SIZE, device=d0.device_id, peer_access=[d1.device_id]) assert d1.device_id in node.peer_access @pytest.mark.agent_authored(model="gpt-5.6-sol") def test_alloc_memory_type_host(init_cuda): """HOST graph alloc nodes reconstruct memory_type from the driver, not the Python argument.""" skip_if_mempool_unsupported() from cuda.core._utils.cuda_utils import CUDAError g = GraphDefinition() try: node = g.allocate(ALLOC_SIZE, memory_type=GraphMemoryType.HOST) except CUDAError as e: if "CUDA_ERROR_NOT_SUPPORTED" in str(e): pytest.skip("Driver does not support graph alloc memory_type='host'") raise expected_dptr = node.dptr succ = node.record(Device().create_event()) node_ref = weakref.ref(node) del node gc.collect() assert node_ref() is None reconstructed = next(iter(succ.pred)) assert isinstance(reconstructed, AllocNode) assert reconstructed.memory_type == GraphMemoryType.HOST assert reconstructed.dptr == expected_dptr assert reconstructed.dptr != 0 # ============================================================================= # Join API # ============================================================================= @pytest.mark.parametrize("num_branches", [2, 3, 5]) def test_join_merges_branches(init_cuda, num_branches): """join() with multiple branches creates correct dependencies.""" sample_graphdef = GraphDefinition() skip_if_mempool_unsupported() branches = [sample_graphdef.allocate(ALLOC_SIZE) for _ in range(num_branches)] joined = sample_graphdef.join(*branches) assert isinstance(joined, EmptyNode) assert set(joined.pred) == set(branches) # ============================================================================= # Kernel launch # ============================================================================= def test_launch_creates_node(init_cuda): """launch() creates a KernelNode.""" sample_graphdef = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) node = sample_graphdef.launch(config, kernel) assert isinstance(node, KernelNode) def test_launch_chain_dependencies(init_cuda): """Chained launches create correct dependencies.""" sample_graphdef = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) n1 = sample_graphdef.launch(config, kernel) n2 = n1.launch(config, kernel) n3 = n2.launch(config, kernel) assert n1 in n2.pred assert n2 in n3.pred assert n1 not in n3.pred # ============================================================================= # Instantiation and execution # ============================================================================= _SENTINEL_UPLOAD_STREAM = "USE_TEST_STREAM" _INSTANTIATE_ONLY_OPTIONS = [ pytest.param({"no_arg": True}, id="no-options"), pytest.param({"options": None}, id="none-options"), pytest.param( {"options": GraphCompleteOptions(auto_free_on_launch=True, use_node_priority=True)}, id="all-bool-flags", ), ] _EXECUTE_OPTIONS = [ pytest.param({}, id="no-options"), pytest.param({"options": GraphCompleteOptions(auto_free_on_launch=True)}, id="auto-free"), pytest.param({"options": GraphCompleteOptions(use_node_priority=True)}, id="node-priority"), pytest.param( {"options": GraphCompleteOptions(upload_stream=_SENTINEL_UPLOAD_STREAM)}, id="upload-stream", ), ] def _instantiate(graph_definition, kwargs, stream=None): """Call graph_definition.instantiate() with the given kwargs, resolving sentinels.""" if "no_arg" in kwargs: return graph_definition.instantiate() opts = kwargs.get("options") if opts is not None and opts.upload_stream == _SENTINEL_UPLOAD_STREAM: opts = GraphCompleteOptions( auto_free_on_launch=opts.auto_free_on_launch, upload_stream=stream, device_launch=opts.device_launch, use_node_priority=opts.use_node_priority, ) return graph_definition.instantiate(options=opts) def _instantiate_and_upload(graph_definition, kwargs, stream): """Instantiate and upload, handling upload_stream option.""" graph = _instantiate(graph_definition, kwargs, stream) if not (kwargs.get("options") and kwargs["options"].upload_stream): graph.upload(stream) return graph @pytest.mark.parametrize("inst_kwargs", _INSTANTIATE_ONLY_OPTIONS) def test_instantiate_empty_graph(init_cuda, inst_kwargs): """Empty graph can be instantiated.""" sample_graphdef = GraphDefinition() graph = _instantiate(sample_graphdef, inst_kwargs) assert graph is not None @pytest.mark.parametrize("inst_kwargs", _INSTANTIATE_ONLY_OPTIONS) def test_instantiate_with_nodes(init_cuda, inst_kwargs): """Graph with nodes can be instantiated.""" sample_graphdef = GraphDefinition() skip_if_mempool_unsupported() sample_graphdef.allocate(ALLOC_SIZE) sample_graphdef.allocate(ALLOC_SIZE) graph = _instantiate(sample_graphdef, inst_kwargs) assert graph is not None @pytest.mark.skipif(not Device(0).properties.unified_addressing, reason="requires unified addressing") def test_instantiate_and_execute_kernel_device_launch(init_cuda): """Kernel-only graph can be instantiated with device_launch flag.""" sample_graphdef = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) sample_graphdef.launch(config, kernel) opts = GraphCompleteOptions(device_launch=True) graph = sample_graphdef.instantiate(options=opts) stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() @pytest.mark.parametrize("inst_kwargs", _EXECUTE_OPTIONS) def test_instantiate_and_execute_kernel(init_cuda, inst_kwargs): """Graph with kernel can be instantiated and executed.""" sample_graphdef = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) sample_graphdef.launch(config, kernel) stream = Device().create_stream() graph = _instantiate_and_upload(sample_graphdef, inst_kwargs, stream) graph.launch(stream) stream.sync() @pytest.mark.parametrize("inst_kwargs", _EXECUTE_OPTIONS) def test_instantiate_and_execute_alloc_free(init_cuda, inst_kwargs): """Graph with alloc/free can be executed.""" sample_graphdef = GraphDefinition() skip_if_mempool_unsupported() alloc = sample_graphdef.allocate(ALLOC_SIZE) alloc.deallocate(alloc.dptr) stream = Device().create_stream() graph = _instantiate_and_upload(sample_graphdef, inst_kwargs, stream) graph.launch(stream) stream.sync() @pytest.mark.parametrize("inst_kwargs", _EXECUTE_OPTIONS) def test_instantiate_and_execute_memset(init_cuda, inst_kwargs): """Graph with alloc/memset/free can be executed.""" sample_graphdef = GraphDefinition() skip_if_mempool_unsupported() alloc = sample_graphdef.allocate(ALLOC_SIZE) ms = alloc.memset(alloc.dptr, 0xAB, ALLOC_SIZE) ms.deallocate(alloc.dptr) stream = Device().create_stream() graph = _instantiate_and_upload(sample_graphdef, inst_kwargs, stream) graph.launch(stream) stream.sync() @pytest.mark.parametrize("inst_kwargs", _EXECUTE_OPTIONS) def test_instantiate_and_execute_memcpy(init_cuda, inst_kwargs): """Graph with alloc/memset/memcpy/free can be executed and data is copied.""" sample_graphdef = GraphDefinition() skip_if_mempool_unsupported() import ctypes src_alloc = sample_graphdef.allocate(ALLOC_SIZE) dst_alloc = sample_graphdef.allocate(ALLOC_SIZE) dep = sample_graphdef.join(src_alloc, dst_alloc) ms = dep.memset(src_alloc.dptr, 0xAB, ALLOC_SIZE) cp = ms.memcpy(dst_alloc.dptr, src_alloc.dptr, ALLOC_SIZE) cp.deallocate(src_alloc.dptr) stream = Device().create_stream() graph = _instantiate_and_upload(sample_graphdef, inst_kwargs, stream) graph.launch(stream) stream.sync() host_buf = (ctypes.c_ubyte * ALLOC_SIZE)() from cuda.bindings import driver as drv drv.cuMemcpyDtoH(host_buf, dst_alloc.dptr, ALLOC_SIZE) assert all(b == 0xAB for b in host_buf) def test_instantiate_and_execute_child_graph(init_cuda): """Graph with embedded child graph can be executed.""" sample_graphdef = GraphDefinition() child = GraphDefinition() mod = compile_common_kernels() kernel = mod.get_kernel("empty_kernel") config = LaunchConfig(grid=1, block=1) child.launch(config, kernel) sample_graphdef.embed(child) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() def test_instantiate_and_execute_host_callback(init_cuda): """Graph with host callback can be executed and callback is invoked.""" sample_graphdef = GraphDefinition() results = [] def my_callback(): results.append(42) sample_graphdef.callback(my_callback) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() assert results == [42] def test_instantiate_and_execute_host_callback_cfunc(init_cuda): """Graph with ctypes function pointer callback can be executed.""" sample_graphdef = GraphDefinition() import ctypes CALLBACK = ctypes.CFUNCTYPE(None, ctypes.c_void_p) called = [False] @CALLBACK def raw_fn(data): called[0] = True sample_graphdef.callback(raw_fn) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() assert called[0] def test_host_callback_cfunc_with_user_data(init_cuda): """Host callback with bytes user_data passes data to C function.""" sample_graphdef = GraphDefinition() import ctypes CALLBACK = ctypes.CFUNCTYPE(None, ctypes.c_void_p) result = [0] @CALLBACK def read_byte(data): result[0] = ctypes.cast(data, ctypes.POINTER(ctypes.c_uint8))[0] sample_graphdef.callback(read_byte, user_data=bytes([0xAB])) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() assert result[0] == 0xAB def test_host_callback_user_data_rejected_for_python_callable(init_cuda): """user_data is rejected for Python callables.""" sample_graphdef = GraphDefinition() with pytest.raises(ValueError, match="user_data is only supported"): sample_graphdef.callback(lambda: None, user_data=b"hello") _INCOMPATIBLE_CTYPES_HOST_CALLBACKS = [ pytest.param(ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p), id="bad-restype"), pytest.param(ctypes.CFUNCTYPE(None, ctypes.c_int), id="bad-argtype"), pytest.param(ctypes.CFUNCTYPE(None), id="missing-arg"), pytest.param(ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_void_p), id="extra-arg"), ] # Prototypes that declare CUhostFn but differ in ctypes bookkeeping. ctypes # builds the same thunk for all of them, so all must be accepted. _COMPATIBLE_CTYPES_HOST_CALLBACKS = [ pytest.param(ctypes.CFUNCTYPE(None, ctypes.c_void_p), id="cfunctype"), pytest.param(ctypes.CFUNCTYPE(None, ctypes.c_void_p, use_errno=True), id="use-errno"), pytest.param(ctypes.PYFUNCTYPE(None, ctypes.c_void_p), id="pyfunctype"), ] @pytest.mark.agent_authored(model="cursor-grok-4.5") @pytest.mark.parametrize("callback_type", _INCOMPATIBLE_CTYPES_HOST_CALLBACKS) def test_host_callback_ctypes_rejects_incompatible_signature(init_cuda, callback_type): """Incompatible ctypes prototypes are rejected before CUDA sees them.""" sample_graphdef = GraphDefinition() with pytest.raises(TypeError, match="CUhostFn"): sample_graphdef.callback(callback_type(0)) @pytest.mark.agent_authored(model="cursor-grok-4.5") def test_host_callback_ctypes_update_rejects_incompatible_signature(init_cuda): """HostCallbackNode.update applies the same ctypes ABI check.""" sample_graphdef = GraphDefinition() good_type = ctypes.CFUNCTYPE(None, ctypes.c_void_p) bad_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p) @good_type def good(data): pass node = sample_graphdef.callback(good) with pytest.raises(TypeError, match="CUhostFn"): node.update(bad_type(0)) @pytest.mark.agent_authored(model="claude-opus-5") @pytest.mark.parametrize("callback_type", _COMPATIBLE_CTYPES_HOST_CALLBACKS) def test_host_callback_ctypes_accepts_equivalent_prototypes(init_cuda, callback_type): """Prototypes that declare CUhostFn are accepted and run.""" sample_graphdef = GraphDefinition() called = [False] @callback_type def raw_fn(data): called[0] = True sample_graphdef.callback(raw_fn) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() assert called[0] @pytest.mark.agent_authored(model="cursor-grok-4.5") @pytest.mark.skipif(sys.platform != "win32", reason="WINFUNCTYPE is Windows-only") def test_host_callback_ctypes_accepts_winfunctype(init_cuda): """On Windows, WINFUNCTYPE matches CUDA_CB (__stdcall) and is accepted.""" sample_graphdef = GraphDefinition() callback_type = ctypes.WINFUNCTYPE(None, ctypes.c_void_p) called = [False] @callback_type def raw_fn(data): called[0] = True sample_graphdef.callback(raw_fn) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() assert called[0] def test_instantiate_and_execute_event_record_wait(init_cuda): """Graph with event record and wait nodes can be executed.""" sample_graphdef = GraphDefinition() event = Device().create_event() rec = sample_graphdef.record(event) rec.wait(event) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() # ============================================================================= # Conditional nodes # ============================================================================= def _skip_unless_cc_90(): if Device(0).compute_capability < (9, 0): pytest.skip("Conditional node execution requires CC >= 9.0 (Hopper)") def test_instantiate_and_execute_if_then(init_cuda): """If-conditional node: body executes only when condition is non-zero.""" sample_graphdef = GraphDefinition() _skip_unless_cc_90() skip_if_mempool_unsupported() import ctypes from helpers.graph_kernels import compile_conditional_kernels condition = sample_graphdef.create_condition(default_value=0) mod = compile_conditional_kernels(int) set_handle = mod.get_kernel("set_handle") add_one = mod.get_kernel("add_one") alloc = sample_graphdef.allocate(ctypes.sizeof(ctypes.c_int)) ms = alloc.memset(alloc.dptr, 0, ctypes.sizeof(ctypes.c_int)) setter = ms.launch(LaunchConfig(grid=1, block=1), set_handle, condition, 1) if_node = setter.if_then(condition) if_node.then.launch(LaunchConfig(grid=1, block=1), add_one, alloc.dptr) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() result = (ctypes.c_int * 1)() from cuda.bindings import driver as drv drv.cuMemcpyDtoH(result, alloc.dptr, ctypes.sizeof(ctypes.c_int)) assert result[0] == 1 def test_instantiate_and_execute_if_else(init_cuda): """If-else node: then or else branch executes based on condition.""" sample_graphdef = GraphDefinition() _skip_unless_cc_90() skip_if_mempool_unsupported() import ctypes from helpers.graph_kernels import compile_conditional_kernels condition = sample_graphdef.create_condition(default_value=0) mod = compile_conditional_kernels(int) set_handle = mod.get_kernel("set_handle") add_one = mod.get_kernel("add_one") alloc = sample_graphdef.allocate(ctypes.sizeof(ctypes.c_int)) ms = alloc.memset(alloc.dptr, 0, ctypes.sizeof(ctypes.c_int)) setter = ms.launch(LaunchConfig(grid=1, block=1), set_handle, condition, 0) ie_node = setter.if_else(condition) ie_node.then.launch(LaunchConfig(grid=1, block=1), add_one, alloc.dptr) n1 = ie_node.else_.launch(LaunchConfig(grid=1, block=1), add_one, alloc.dptr) n1.launch(LaunchConfig(grid=1, block=1), add_one, alloc.dptr) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() result = (ctypes.c_int * 1)() from cuda.bindings import driver as drv drv.cuMemcpyDtoH(result, alloc.dptr, ctypes.sizeof(ctypes.c_int)) assert result[0] == 2 def test_instantiate_and_execute_switch(init_cuda): """Switch node: selected branch executes based on condition value.""" sample_graphdef = GraphDefinition() _skip_unless_cc_90() skip_if_mempool_unsupported() import ctypes from helpers.graph_kernels import compile_conditional_kernels condition = sample_graphdef.create_condition(default_value=0) mod = compile_conditional_kernels(int) set_handle = mod.get_kernel("set_handle") add_one = mod.get_kernel("add_one") alloc = sample_graphdef.allocate(ctypes.sizeof(ctypes.c_int)) ms = alloc.memset(alloc.dptr, 0, ctypes.sizeof(ctypes.c_int)) setter = ms.launch(LaunchConfig(grid=1, block=1), set_handle, condition, 2) sw_node = setter.switch(condition, 4) for branch in sw_node.branches: branch.launch(LaunchConfig(grid=1, block=1), add_one, alloc.dptr) graph = sample_graphdef.instantiate() stream = Device().create_stream() graph.upload(stream) graph.launch(stream) stream.sync() result = (ctypes.c_int * 1)() from cuda.bindings import driver as drv drv.cuMemcpyDtoH(result, alloc.dptr, ctypes.sizeof(ctypes.c_int)) assert result[0] == 1 def test_conditional_node_type_preserved_by_nodes(init_cuda): """Conditional nodes appear as ConditionalNode base when read back from graph.""" sample_graphdef = GraphDefinition() condition = try_create_condition(sample_graphdef) if_node = sample_graphdef.if_then(condition) assert isinstance(if_node, IfNode) all_nodes = sample_graphdef.nodes() matched = [n for n in all_nodes if n == if_node] assert len(matched) == 1 assert isinstance(matched[0], ConditionalNode) # ============================================================================= # Debug output # ============================================================================= def test_debug_dot_print_creates_file(init_cuda, tmp_path): """debug_dot_print writes a DOT file.""" sample_graphdef = GraphDefinition() dot_file = tmp_path / "graph.dot" skip_if_mempool_unsupported() sample_graphdef.allocate(ALLOC_SIZE) sample_graphdef.debug_dot_print(str(dot_file)) assert dot_file.exists() content = dot_file.read_text() assert "digraph" in content def test_debug_dot_print_with_options(init_cuda, tmp_path): """debug_dot_print accepts GraphDebugPrintOptions.""" sample_graphdef = GraphDefinition() dot_file = tmp_path / "graph.dot" skip_if_mempool_unsupported() sample_graphdef.allocate(ALLOC_SIZE) options = GraphDebugPrintOptions(verbose=True, handles=True) sample_graphdef.debug_dot_print(str(dot_file), options) assert dot_file.exists() def test_debug_dot_print_invalid_options(init_cuda, tmp_path): """debug_dot_print rejects invalid options type.""" sample_graphdef = GraphDefinition() dot_file = tmp_path / "graph.dot" skip_if_mempool_unsupported() sample_graphdef.allocate(ALLOC_SIZE) with pytest.raises(TypeError, match="options must be a GraphDebugPrintOptions"): sample_graphdef.debug_dot_print(str(dot_file), "invalid")