Skip to content

Commit d47ef9f

Browse files
committed
test(cuda.core): cover API validation and resource lifecycles
Exercise validation failures, resource ownership, and recovery paths using real CUDA resources. Verify tensor-map dtype encoding and exporter lifetime without requiring runtime kernel compilation. Signed-off-by: Rui Luo <ruluo@nvidia.com>
1 parent d4c71ef commit d47ef9f

11 files changed

Lines changed: 771 additions & 5 deletions

‎cuda_core/tests/graph/test_graph_definition_errors.py‎

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
import ctypes
77

88
import pytest
9+
from helpers.contexts import current_context_handle, use_context
910
from helpers.graph_kernels import compile_common_kernels
1011
from helpers.misc import try_create_condition
1112

12-
from cuda.core import Device, LaunchConfig
13-
from cuda.core._utils.cuda_utils import CUDAError
13+
from cuda.core import ContextOptions, Device, LaunchConfig, SMResourceOptions
14+
from cuda.core._utils.cuda_utils import CUDAError, driver, handle_return
15+
from cuda.core._utils.version import driver_version
1416
from cuda.core.graph import (
1517
EmptyNode,
18+
GraphCompleteOptions,
1619
GraphCondition,
1720
GraphDefinition,
1821
)
@@ -150,6 +153,118 @@ def test_multiple_instantiation(init_cuda):
150153
assert g1 is not g2
151154

152155

156+
@pytest.mark.agent_authored(model="gpt-6-sol")
157+
def test_instantiate_rejects_cycle_and_accepts_repaired_graph(init_cuda):
158+
"""Removing a cycle makes an invalid graph executable again."""
159+
definition = GraphDefinition()
160+
called = []
161+
first = definition.empty()
162+
last = first.callback(lambda: called.append(True))
163+
last.succ.add(first)
164+
stream = init_cuda.create_stream()
165+
executable = None
166+
try:
167+
with pytest.raises(RuntimeError, match="invalid structure, such as cycles"):
168+
definition.instantiate()
169+
assert called == []
170+
last.succ.discard(first)
171+
executable = definition.instantiate()
172+
executable.launch(stream)
173+
stream.sync()
174+
assert called == [True]
175+
finally:
176+
if executable is not None:
177+
executable.close()
178+
last.succ.discard(first)
179+
last.destroy()
180+
first.destroy()
181+
stream.close()
182+
183+
184+
@pytest.mark.agent_authored(model="gpt-6-sol")
185+
def test_device_launch_rejects_host_callback(init_cuda):
186+
"""Device-launch graphs reject host callbacks before execution."""
187+
definition = GraphDefinition()
188+
called = []
189+
node = definition.callback(lambda: called.append(True))
190+
try:
191+
with pytest.raises(
192+
RuntimeError, match="device launch failed because the graph contained an unsupported operation"
193+
):
194+
definition.instantiate(GraphCompleteOptions(device_launch=True))
195+
assert called == []
196+
finally:
197+
node.destroy()
198+
199+
200+
@pytest.mark.agent_authored(model="gpt-6-sol")
201+
def test_device_launch_rejects_nodes_from_different_contexts(init_cuda):
202+
"""Device-launch instantiation rejects mixed contexts and accepts a repaired graph."""
203+
if driver_version() < (12, 4, 0):
204+
pytest.skip("green contexts require CUDA driver 12.4 or newer")
205+
if not init_cuda.properties.unified_addressing:
206+
pytest.skip("device launch requires unified addressing")
207+
original_context = current_context_handle()
208+
definition = GraphDefinition()
209+
green_context = first = foreign = executable = stream = None
210+
try:
211+
groups, _ = init_cuda.resources.sm.split(SMResourceOptions(count=None))
212+
green_context = init_cuda.create_context(ContextOptions(resources=[groups[0]]))
213+
module = compile_common_kernels()
214+
kernel = module.get_kernel("empty_kernel")
215+
config = LaunchConfig(grid=1, block=1)
216+
first = definition.launch(config, kernel)
217+
with use_context(init_cuda, green_context):
218+
foreign = definition.launch(config, kernel)
219+
220+
# CUkernel handles are context-independent; check each node's resolved context.
221+
first_params = handle_return(driver.cuGraphKernelNodeGetParams(first.handle))
222+
foreign_params = handle_return(driver.cuGraphKernelNodeGetParams(foreign.handle))
223+
assert int(first_params.ctx) == original_context
224+
assert int(foreign_params.ctx) == int(green_context.handle)
225+
assert int(foreign_params.ctx) != original_context
226+
assert current_context_handle() == original_context
227+
with pytest.raises(RuntimeError, match="nodes belonging to different contexts"):
228+
definition.instantiate(GraphCompleteOptions(device_launch=True))
229+
assert current_context_handle() == original_context
230+
231+
# A single-context graph remains valid after the failed instantiation.
232+
foreign.destroy()
233+
foreign = None
234+
executable = definition.instantiate(GraphCompleteOptions(device_launch=True))
235+
stream = init_cuda.create_stream()
236+
executable.launch(stream)
237+
stream.sync()
238+
assert current_context_handle() == original_context
239+
finally:
240+
if executable is not None:
241+
executable.close()
242+
if foreign is not None:
243+
foreign.destroy()
244+
if first is not None:
245+
first.destroy()
246+
if stream is not None:
247+
stream.close()
248+
if green_context is not None:
249+
green_context.close()
250+
251+
252+
@pytest.mark.agent_authored(model="gpt-6-sol")
253+
def test_instantiate_rejects_unassociated_condition(init_cuda):
254+
"""Instantiation rejects a condition that no conditional node uses."""
255+
if driver_version() < (12, 8, 0):
256+
pytest.skip("Unused graph-condition diagnostics require CUDA driver 12.8 or newer")
257+
definition = GraphDefinition()
258+
# Deliberately leave this graph-owned condition unassociated with a node.
259+
definition.create_condition(default_value=1)
260+
node = definition.empty()
261+
try:
262+
with pytest.raises(RuntimeError, match="conditional handles are not associated with conditional builders"):
263+
definition.instantiate()
264+
finally:
265+
node.destroy()
266+
267+
153268
def test_unmatched_alloc_succeeds(init_cuda):
154269
"""Alloc without corresponding free is valid (graph-scoped lifetime)."""
155270
_skip_if_no_mempool()

‎cuda_core/tests/test_device.py‎

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
import contextlib
5+
import ctypes
56
import pickle
67
import subprocess
78
import sys
89
from concurrent.futures import ThreadPoolExecutor
910

1011
import pytest
12+
from helpers.buffers import DummyDeviceMemoryResource
1113
from helpers.contexts import (
1214
assert_device_operations_use_bound_context,
1315
current_context_handle,
@@ -18,7 +20,7 @@
1820
import cuda.core
1921
from cuda.bindings import driver, runtime
2022
from cuda.core import Device, StreamOptions
21-
from cuda.core._utils.cuda_utils import ComputeCapability, handle_return
23+
from cuda.core._utils.cuda_utils import ComputeCapability, CUDAError, handle_return
2224
from cuda.core._utils.version import driver_version
2325

2426

@@ -58,6 +60,95 @@ def test_device_set_current(deinit_cuda):
5860
assert handle_return(driver.cuCtxGetCurrent()) is not None
5961

6062

63+
@pytest.mark.parametrize("ordinal_kind", ["negative", "one-past-end"])
64+
@pytest.mark.agent_authored(model="gpt-6-sol")
65+
def test_device_rejects_out_of_range_ordinal(init_cuda, ordinal_kind):
66+
"""Invalid ordinals preserve the current context and cached Device."""
67+
device_count = len(Device.get_all_devices())
68+
ordinal = -1 if ordinal_kind == "negative" else device_count
69+
original_context = current_context_handle()
70+
with pytest.raises(ValueError):
71+
Device(ordinal)
72+
assert current_context_handle() == original_context
73+
assert Device(init_cuda.device_id) is init_cuda
74+
75+
76+
@pytest.mark.agent_authored(model="gpt-6-sol")
77+
def test_device_context_requires_initialization_on_new_thread(init_cuda):
78+
"""Initialization on one thread does not initialize another thread's Device."""
79+
device_id = init_cuda.device_id
80+
original_context = current_context_handle()
81+
82+
def worker():
83+
device = Device(device_id)
84+
assert current_context_handle() == 0
85+
with pytest.raises(CUDAError):
86+
_ = device.context
87+
with pytest.raises(CUDAError):
88+
device.create_stream()
89+
device.set_current()
90+
stream = None
91+
try:
92+
stream = device.create_stream()
93+
assert int(stream.context.handle) == int(device.context.handle)
94+
with device.allocate(4, stream=stream) as buffer:
95+
stream.sync()
96+
handle_return(driver.cuMemsetD8(int(buffer.handle), 0x29, buffer.size))
97+
result = (ctypes.c_ubyte * 4)()
98+
handle_return(driver.cuMemcpyDtoH(ctypes.addressof(result), int(buffer.handle), 4))
99+
assert bytes(result) == b"\x29" * 4
100+
stream.sync()
101+
finally:
102+
if stream is not None:
103+
stream.close()
104+
handle_return(driver.cuCtxPopCurrent())
105+
106+
with ThreadPoolExecutor(max_workers=1) as executor:
107+
executor.submit(worker).result()
108+
assert current_context_handle() == original_context
109+
110+
111+
@pytest.mark.agent_authored(model="gpt-6-sol")
112+
def test_device_memory_resource_controls_allocation(init_cuda):
113+
"""Device.allocate uses the public memory resource assigned to the device."""
114+
device = init_cuda
115+
original_resource = device.memory_resource
116+
replacement = DummyDeviceMemoryResource(device)
117+
try:
118+
device.memory_resource = replacement
119+
assert device.memory_resource is replacement
120+
with device.allocate(16, stream=device.default_stream) as buffer:
121+
assert buffer.memory_resource is replacement
122+
handle_return(driver.cuMemsetD8(int(buffer.handle), 0x5C, buffer.size))
123+
result = (ctypes.c_ubyte * 16)()
124+
handle_return(driver.cuMemcpyDtoH(ctypes.addressof(result), int(buffer.handle), 16))
125+
assert bytes(result) == b"\x5c" * 16
126+
finally:
127+
device.memory_resource = original_resource
128+
assert device.memory_resource is original_resource
129+
130+
131+
@pytest.mark.agent_authored(model="gpt-6-sol")
132+
def test_invalid_memory_resource_assignment_preserves_resource(init_cuda):
133+
"""A rejected replacement leaves the existing allocator operational."""
134+
device = init_cuda
135+
original_resource = device.memory_resource
136+
try:
137+
with pytest.raises(TypeError):
138+
device.memory_resource = object()
139+
assert device.memory_resource is original_resource
140+
with device.allocate(4, stream=device.default_stream) as buffer:
141+
device.default_stream.sync()
142+
assert buffer.memory_resource is original_resource
143+
handle_return(driver.cuMemsetD8(int(buffer.handle), 0x3A, buffer.size))
144+
result = (ctypes.c_ubyte * 4)()
145+
handle_return(driver.cuMemcpyDtoH(ctypes.addressof(result), int(buffer.handle), 4))
146+
assert bytes(result) == b"\x3a" * 4
147+
device.default_stream.sync()
148+
finally:
149+
device.memory_resource = original_resource
150+
151+
61152
@pytest.mark.agent_authored(model="gpt-5.6-sol")
62153
def test_primary_context_cleanup_is_safe_during_python_shutdown(init_cuda, tmp_path):
63154
result = subprocess.run(

‎cuda_core/tests/test_green_context.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import numpy as np
66
import pytest
7-
from helpers.contexts import assert_device_operations_use_bound_context, use_context
7+
from helpers.contexts import assert_device_operations_use_bound_context, current_context_handle, use_context
88

99
from cuda.core import (
1010
ContextOptions,
@@ -208,6 +208,24 @@ def test_create_context_requires_resources(init_cuda):
208208
init_cuda.create_context(object())
209209

210210

211+
@pytest.mark.agent_authored(model="gpt-6-sol")
212+
def test_create_context_rejects_empty_resources(init_cuda):
213+
"""An empty resource list cannot create a green context."""
214+
original_context = current_context_handle()
215+
with pytest.raises(ValueError):
216+
init_cuda.create_context(ContextOptions(resources=[]))
217+
assert current_context_handle() == original_context
218+
219+
220+
@pytest.mark.agent_authored(model="gpt-6-sol")
221+
def test_create_context_rejects_unsupported_resource(init_cuda):
222+
"""A resource of an unsupported type leaves the current context unchanged."""
223+
original_context = current_context_handle()
224+
with pytest.raises(TypeError):
225+
init_cuda.create_context(ContextOptions(resources=[object()]))
226+
assert current_context_handle() == original_context
227+
228+
211229
@pytest.mark.agent_authored(model="claude-opus-4.8")
212230
def test_context_handle_alias_and_closed_queries(init_cuda, sm_resource):
213231
"""``Context._handle`` mirrors ``.handle``; after a (non-current) green

‎cuda_core/tests/test_launcher.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,3 +1024,64 @@ class SubclassedHandle(driver.CUgraphConditionalHandle):
10241024
dev = Device()
10251025
dev.set_current()
10261026
_assert_kernel_sees_ull(dev, handle, _UINT64_HANDLE_VALUE)
1027+
1028+
1029+
@pytest.mark.parametrize("allocation", ["managed", "virtual"])
1030+
@pytest.mark.agent_authored(model="gpt-6-sol")
1031+
def test_launch_buffer_subclasses_preserve_kernel_arguments(init_cuda, allocation):
1032+
"""Managed and virtual buffers marshal their device pointers into kernels."""
1033+
from contextlib import ExitStack
1034+
1035+
from cuda.bindings import driver
1036+
from cuda.core import ManagedBuffer
1037+
from cuda.core._utils.cuda_utils import handle_return
1038+
1039+
device = init_cuda
1040+
if allocation == "managed" and not device.properties.managed_memory:
1041+
pytest.skip("Device does not support managed memory")
1042+
if allocation == "virtual" and not device.properties.virtual_memory_management_supported:
1043+
pytest.skip("Device does not support CUDA virtual memory")
1044+
source = np.arange(16, dtype=np.int32)
1045+
actual = np.empty_like(source)
1046+
stream = device.create_stream()
1047+
buffer = program = managed_pointer = None
1048+
try:
1049+
if allocation == "managed":
1050+
managed_pointer = handle_return(
1051+
driver.cuMemAllocManaged(source.nbytes, driver.CUmemAttach_flags.CU_MEM_ATTACH_GLOBAL)
1052+
)
1053+
buffer = ManagedBuffer.from_handle(managed_pointer, source.nbytes)
1054+
assert isinstance(buffer, ManagedBuffer)
1055+
else:
1056+
from cuda.core import VirtualMemoryBuffer, VirtualMemoryResource, VirtualMemoryResourceOptions
1057+
1058+
resource = VirtualMemoryResource(device, VirtualMemoryResourceOptions(handle_type=None))
1059+
buffer = resource.allocate(source.nbytes, stream=stream)
1060+
assert isinstance(buffer, VirtualMemoryBuffer)
1061+
handle_return(driver.cuMemcpyHtoDAsync(buffer.handle, source.ctypes.data, source.nbytes, stream.handle))
1062+
program = Program(
1063+
'extern "C" __global__ void update(int* data, int offset) { '
1064+
"int i = threadIdx.x; data[i] = data[i] * 3 + offset; }",
1065+
"c++",
1066+
)
1067+
module = program.compile("cubin")
1068+
kernel = module.get_kernel("update")
1069+
launch(stream, LaunchConfig(grid=1, block=16), kernel, buffer, np.int32(7))
1070+
stream.sync()
1071+
handle_return(driver.cuMemcpyDtoH(actual.ctypes.data, buffer.handle, actual.nbytes))
1072+
np.testing.assert_array_equal(actual, source * 3 + 7)
1073+
buffer.close()
1074+
assert buffer.is_closed
1075+
with pytest.raises(RuntimeError, match="Buffer has been closed"):
1076+
launch(stream, LaunchConfig(grid=1, block=16), kernel, buffer, np.int32(7))
1077+
finally:
1078+
# Attempt every cleanup even if synchronization or another cleanup fails.
1079+
with ExitStack() as cleanup:
1080+
cleanup.callback(stream.close)
1081+
if program is not None:
1082+
cleanup.callback(program.close)
1083+
if managed_pointer is not None:
1084+
cleanup.callback(lambda: handle_return(driver.cuMemFree(managed_pointer)))
1085+
if buffer is not None:
1086+
cleanup.callback(buffer.close)
1087+
stream.sync()

‎cuda_core/tests/test_memory.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,6 +1940,38 @@ def test_vmm_buffers_alive_at_shutdown_are_freed_quietly(init_cuda):
19401940
assert result.stderr == ""
19411941

19421942

1943+
@pytest.mark.parametrize(
1944+
"options",
1945+
[
1946+
pytest.param({"addr_align": 3}, id="invalid-alignment"),
1947+
pytest.param({"peers": (-1,)}, id="invalid-peer"),
1948+
],
1949+
)
1950+
@pytest.mark.thread_unsafe(reason="records process-global warnings")
1951+
@pytest.mark.agent_authored(model="gpt-6-sol")
1952+
def test_failed_vmm_allocation_does_not_block_another_resource(init_cuda, options):
1953+
"""A failed allocation leaves its config unchanged and another VMM resource usable."""
1954+
device = _vmm_device_or_skip()
1955+
invalid_resource = _vmm_resource(device, **options)
1956+
error_match = "CUDA_ERROR_INVALID_VALUE" if "addr_align" in options else r"CUDA_ERROR_INVALID_(VALUE|DEVICE)"
1957+
with assert_no_cuda_warning(), pytest.raises(CUDAError, match=error_match):
1958+
invalid_resource.allocate(4096)
1959+
for name, value in options.items():
1960+
assert getattr(invalid_resource.config, name) == value
1961+
1962+
# Use a separate resource with valid options; the invalid configuration is unchanged.
1963+
resource = _vmm_resource(device)
1964+
buffer = resource.allocate(4096)
1965+
ptr, size = int(buffer.handle), buffer.size
1966+
try:
1967+
_vmm_fill(buffer, 0x5C)
1968+
assert _vmm_read(buffer, 0, 16) == bytes([0x5C]) * 16
1969+
finally:
1970+
with assert_no_cuda_warning():
1971+
buffer.close()
1972+
_vmm_assert_released([(ptr, size)])
1973+
1974+
19431975
@pytest.mark.agent_authored(model="claude-fable-5-1")
19441976
@pytest.mark.thread_unsafe(reason="records process-global warnings")
19451977
def test_vmm_failed_grow_leaves_input_intact(init_cuda):

0 commit comments

Comments
 (0)