Skip to content

Commit fb6908e

Browse files
committed
coverage: add tests for memory, launcher, linker, program, and utils coverage gaps
1 parent 7a9ee61 commit fb6908e

6 files changed

Lines changed: 414 additions & 1 deletion

File tree

cuda_core/tests/graph/test_graph_memory_resource.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,36 @@ def test_gmr_check_capture_state(mempool_device, mode):
290290
gb.end_building().complete()
291291

292292

293+
def test_graph_memory_resource_attributes_direct_init_raises():
294+
"""GraphMemoryResourceAttributes cannot be constructed directly."""
295+
from cuda.core._memory._graph_memory_resource import GraphMemoryResourceAttributes
296+
297+
with pytest.raises(RuntimeError, match="cannot be instantiated directly"):
298+
GraphMemoryResourceAttributes()
299+
300+
301+
def test_graph_memory_resource_accessibility_flags(init_cuda):
302+
"""GraphMemoryResource exposes expected accessibility flags and device_id."""
303+
device = Device()
304+
gmr = GraphMemoryResource(device)
305+
assert gmr.is_device_accessible is True
306+
assert gmr.is_host_accessible is False
307+
assert gmr.device_id == int(device)
308+
309+
310+
def test_graph_memory_resource_attributes_repr(mempool_device):
311+
"""GraphMemoryResourceAttributes.__repr__ includes the class name and the 4 documented attributes."""
312+
device = mempool_device
313+
gmr = GraphMemoryResource(device)
314+
r = repr(gmr.attributes)
315+
assert r.startswith("GraphMemoryResourceAttributes(")
316+
assert r.endswith(")")
317+
assert "reserved_mem_current=" in r
318+
assert "reserved_mem_high=" in r
319+
assert "used_mem_current=" in r
320+
assert "used_mem_high=" in r
321+
322+
293323
@pytest.mark.parametrize("mode", ["global", "thread_local", "relaxed"])
294324
def test_dmr_check_capture_state(mempool_device, mode):
295325
"""

cuda_core/tests/test_launcher.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,64 @@ def test_launch_config_native_conversion(init_cuda):
125125
pytest.skip("Driver or GPU not new enough for thread block clusters")
126126

127127

128+
def test_to_native_launch_config_no_cluster():
129+
"""Covers the no-cluster path of _to_native_launch_config; no Hopper+ required."""
130+
from cuda.core._launch_config import _to_native_launch_config
131+
132+
config = LaunchConfig(grid=(4, 5, 6), block=(7, 8, 9), shmem_size=128)
133+
native = _to_native_launch_config(config)
134+
assert native.gridDimX == 4, f"Expected gridDimX=4, got {native.gridDimX}"
135+
assert native.gridDimY == 5, f"Expected gridDimY=5, got {native.gridDimY}"
136+
assert native.gridDimZ == 6, f"Expected gridDimZ=6, got {native.gridDimZ}"
137+
assert native.blockDimX == 7, f"Expected blockDimX=7, got {native.blockDimX}"
138+
assert native.blockDimY == 8, f"Expected blockDimY=8, got {native.blockDimY}"
139+
assert native.blockDimZ == 9, f"Expected blockDimZ=9, got {native.blockDimZ}"
140+
assert native.sharedMemBytes == 128, f"Expected sharedMemBytes=128, got {native.sharedMemBytes}"
141+
assert native.numAttrs == 0, f"Expected numAttrs=0, got {native.numAttrs}"
142+
assert list(native.attrs) == [], f"Expected empty attrs, got {list(native.attrs)}"
143+
144+
145+
def test_launch_config_cooperative_unsupported(monkeypatch):
146+
"""LaunchConfig(is_cooperative=True) raises when device does not support it."""
147+
from cuda.core import _launch_config as _lc_mod
148+
149+
class _FakeProps:
150+
cooperative_launch = False
151+
152+
class _FakeDev:
153+
properties = _FakeProps()
154+
155+
monkeypatch.setattr(_lc_mod, "Device", lambda: _FakeDev())
156+
with pytest.raises(CUDAError, match="cooperative kernels are not supported"):
157+
LaunchConfig(grid=1, block=1, is_cooperative=True)
158+
159+
160+
def test_to_native_launch_config_cooperative(monkeypatch):
161+
"""Covers the is_cooperative branch of _to_native_launch_config; Device is mocked so it runs on any GPU."""
162+
from cuda.bindings import driver
163+
from cuda.core import _launch_config as _lc_mod
164+
from cuda.core._launch_config import _to_native_launch_config
165+
166+
class _FakeProps:
167+
cooperative_launch = True
168+
169+
class _FakeDev:
170+
properties = _FakeProps()
171+
172+
monkeypatch.setattr(_lc_mod, "Device", lambda: _FakeDev())
173+
174+
config = LaunchConfig(grid=2, block=4, is_cooperative=True)
175+
native = _to_native_launch_config(config)
176+
assert native.gridDimX == 2
177+
assert native.blockDimX == 4
178+
assert native.numAttrs == 1
179+
attr = native.attrs[0]
180+
assert attr.id == driver.CUlaunchAttributeID.CU_LAUNCH_ATTRIBUTE_COOPERATIVE, (
181+
f"Expected CU_LAUNCH_ATTRIBUTE_COOPERATIVE, got {attr.id}"
182+
)
183+
assert attr.value.cooperative == 1, f"Expected cooperative=1, got {attr.value.cooperative}"
184+
185+
128186
def test_launch_invalid_values(init_cuda):
129187
code = 'extern "C" __global__ void my_kernel() {}'
130188
program = Program(code, SourceCodeType.CXX)
@@ -403,7 +461,7 @@ class MyFloat(ctypes.c_float):
403461
class MyBool(ctypes.c_bool):
404462
pass
405463

406-
# These should NOT raise they should be handled via isinstance fallback
464+
# These should NOT raise; they should be handled via isinstance fallback
407465
holder = ParamHolder([MyInt32(42), MyFloat(3.14), MyBool(True)])
408466
assert holder.ptr != 0
409467

cuda_core/tests/test_linker.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,93 @@ def test_which_backend_is_not_property(self):
280280
"""
281281
attr = inspect.getattr_static(Linker, "which_backend")
282282
assert not isinstance(attr, property)
283+
284+
285+
@pytest.fixture
286+
def driver_binding(monkeypatch):
287+
"""Pin _linker._driver to the real driver module so driver-backend tests run under any backend."""
288+
from cuda.bindings import driver
289+
290+
monkeypatch.setattr(_linker, "_driver", driver)
291+
return driver
292+
293+
294+
def test_prepare_driver_options_all_supported(driver_binding):
295+
"""Exercise every supported branch of _prepare_driver_options."""
296+
driver = driver_binding
297+
opts = LinkerOptions(
298+
arch="sm_80",
299+
max_register_count=32,
300+
verbose=True,
301+
link_time_optimization=True,
302+
optimization_level=2,
303+
debug=True,
304+
lineinfo=True,
305+
no_cache=True,
306+
)
307+
formatted, keys = opts._prepare_driver_options()
308+
assert len(formatted) == len(keys)
309+
assert len(keys) == 4 + 8 # 4 fixed log-buffer entries + 8 options set above
310+
311+
# Skip log-buffer entries; verify key-to-value mapping (catches swap/dup/wrong-value).
312+
payload_keys = keys[4:]
313+
assert len(set(payload_keys)) == len(payload_keys), f"duplicate option keys: {payload_keys}"
314+
option_to_value = dict(zip(payload_keys, formatted[4:]))
315+
assert option_to_value[driver.CUjit_option.CU_JIT_TARGET] == driver.CUjit_target.CU_TARGET_COMPUTE_80
316+
assert option_to_value[driver.CUjit_option.CU_JIT_MAX_REGISTERS] == 32
317+
assert option_to_value[driver.CUjit_option.CU_JIT_LOG_VERBOSE] == 1
318+
assert option_to_value[driver.CUjit_option.CU_JIT_LTO] == 1
319+
assert option_to_value[driver.CUjit_option.CU_JIT_OPTIMIZATION_LEVEL] == 2
320+
assert option_to_value[driver.CUjit_option.CU_JIT_GENERATE_DEBUG_INFO] == 1
321+
assert option_to_value[driver.CUjit_option.CU_JIT_GENERATE_LINE_INFO] == 1
322+
assert option_to_value[driver.CUjit_option.CU_JIT_CACHE_MODE] == driver.CUjit_cacheMode.CU_JIT_CACHE_OPTION_NONE
323+
324+
325+
@pytest.mark.parametrize(
326+
"kwargs,match",
327+
[
328+
({"ftz": True}, "ftz option is deprecated"),
329+
({"prec_div": True}, "prec_div option is deprecated"),
330+
({"prec_sqrt": True}, "prec_sqrt option is deprecated"),
331+
({"fma": True}, "fma options is deprecated"),
332+
({"kernels_used": "my_kernel"}, "kernels_used is deprecated"),
333+
({"variables_used": "my_var"}, "variables_used is deprecated"),
334+
({"optimize_unused_variables": True}, "optimize_unused_variables is deprecated"),
335+
],
336+
)
337+
def test_prepare_driver_options_deprecated_warnings(driver_binding, kwargs, match):
338+
"""Each driver-deprecated option emits a DeprecationWarning."""
339+
opts = LinkerOptions(**kwargs)
340+
with pytest.warns(DeprecationWarning, match=match):
341+
opts._prepare_driver_options()
342+
343+
344+
@pytest.mark.parametrize(
345+
"kwargs,match",
346+
[
347+
({"time": True}, "time option is not supported by the driver API"),
348+
({"ptx": True}, "ptx option is not supported by the driver API"),
349+
({"ptxas_options": ["-v"]}, "ptxas_options option is not supported by the driver API"),
350+
({"split_compile": 0}, "split_compile option is not supported by the driver API"),
351+
({"split_compile_extended": 1}, "split_compile_extended option is not supported by the driver API"),
352+
],
353+
)
354+
def test_prepare_driver_options_unsupported_raises(driver_binding, kwargs, match):
355+
"""Each nvjitlink-only option raises ValueError on the driver backend."""
356+
opts = LinkerOptions(**kwargs)
357+
with pytest.raises(ValueError, match=match):
358+
opts._prepare_driver_options()
359+
360+
361+
def test_linker_empty_object_codes_raises():
362+
"""Linker with no ObjectCode raises ValueError."""
363+
with pytest.raises(ValueError, match="At least one ObjectCode object must be provided"):
364+
Linker()
365+
366+
367+
def test_as_bytes_nvjitlink_unavailable(monkeypatch):
368+
"""as_bytes('nvjitlink') raises RuntimeError when the backend is unavailable."""
369+
monkeypatch.setattr(_linker, "_use_nvjitlink_backend", False)
370+
opts = LinkerOptions(arch="sm_80")
371+
with pytest.raises(RuntimeError, match="nvJitLink backend is not available"):
372+
opts.as_bytes("nvjitlink")

cuda_core/tests/test_memory.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
DeviceMemoryResource,
3434
DeviceMemoryResourceOptions,
3535
GraphMemoryResource,
36+
LegacyPinnedMemoryResource,
3637
ManagedMemoryResource,
3738
ManagedMemoryResourceOptions,
3839
MemoryResource,
@@ -1760,3 +1761,136 @@ def test_top_level_namespace_excludes_known_leaks():
17601761
public = {n for n in dir(cuda.core) if not n.startswith("_")}
17611762
leaked = {"StridedMemoryView", "args_viewable_as_strided_memory"}
17621763
assert not (public & leaked)
1764+
1765+
1766+
def test_legacy_pinned_allocate_zero_size(init_cuda):
1767+
"""LegacyPinnedMemoryResource.allocate(0) skips the driver call and uses ptr=0."""
1768+
mr = LegacyPinnedMemoryResource()
1769+
buf = mr.allocate(0)
1770+
assert buf.size == 0
1771+
# No driver call was made; handle is the sentinel 0.
1772+
assert int(buf.handle) == 0
1773+
1774+
1775+
def test_legacy_pinned_device_id_raises():
1776+
"""LegacyPinnedMemoryResource.device_id raises; pinned memory is not bound to a GPU."""
1777+
mr = LegacyPinnedMemoryResource()
1778+
with pytest.raises(RuntimeError, match="not bound to any GPU"):
1779+
_ = mr.device_id
1780+
1781+
1782+
def test_synchronous_memory_resource_basic(init_cuda):
1783+
"""_SynchronousMemoryResource exercises properties and allocate paths (zero, non-zero, with-stream)."""
1784+
from cuda.core._memory._legacy import _SynchronousMemoryResource
1785+
1786+
dev = Device()
1787+
mr = _SynchronousMemoryResource(dev.device_id)
1788+
assert mr.is_device_accessible is True
1789+
assert mr.is_host_accessible is False
1790+
assert mr.device_id == dev.device_id
1791+
1792+
# Zero-size allocation takes the ptr=0 fast path.
1793+
zero_buf = mr.allocate(0)
1794+
assert zero_buf.size == 0
1795+
assert int(zero_buf.handle) == 0
1796+
zero_buf.close(stream=None)
1797+
1798+
# Non-zero allocation goes through cuMemAlloc; close with stream=None for
1799+
# the simple path. The explicit-stream close path is covered separately.
1800+
buf = mr.allocate(64)
1801+
assert buf.size == 64
1802+
assert int(buf.handle) != 0
1803+
buf.close(stream=None)
1804+
1805+
# allocate(size, stream=stream) exercises Stream_accept validation on the
1806+
# allocate side (cuMemAlloc is synchronous so the stream is accepted but unused).
1807+
stream = dev.create_stream()
1808+
buf2 = mr.allocate(32, stream=stream)
1809+
assert buf2.size == 32
1810+
assert int(buf2.handle) != 0
1811+
buf2.close(stream=None)
1812+
stream.close()
1813+
1814+
1815+
def test_synchronous_memory_resource_deallocate_accepts_stream(init_cuda):
1816+
"""_SynchronousMemoryResource.deallocate accepts an explicit stream."""
1817+
from cuda.core._memory._legacy import _SynchronousMemoryResource
1818+
1819+
dev = Device()
1820+
mr = _SynchronousMemoryResource(dev.device_id)
1821+
buf = mr.allocate(64)
1822+
stream = dev.create_stream()
1823+
buf.close(stream=stream)
1824+
stream.close()
1825+
1826+
1827+
@pytest.mark.parametrize(
1828+
("method", "spec", "match"),
1829+
[
1830+
("_access_to_flags", "bogus", "Unknown access spec"),
1831+
("_allocation_type_to_driver", "bogus", "Unsupported allocation_type"),
1832+
("_location_type_to_driver", "bogus", "Unsupported location_type"),
1833+
("_handle_type_to_driver", "bogus", "Unsupported handle_type"),
1834+
("_granularity_to_driver", "bogus", "Unsupported granularity"),
1835+
],
1836+
)
1837+
def test_vmm_options_spec_validators_raise(method, spec, match):
1838+
"""Every VMM spec validator static method rejects unknown strings with ValueError."""
1839+
fn = getattr(VirtualMemoryResourceOptions, method)
1840+
with pytest.raises(ValueError, match=match):
1841+
fn(spec)
1842+
1843+
1844+
def test_vmm_options_handle_type_win32_raises():
1845+
"""_handle_type_to_driver raises NotImplementedError for 'win32'."""
1846+
with pytest.raises(NotImplementedError, match="win32 is currently not supported"):
1847+
VirtualMemoryResourceOptions._handle_type_to_driver("win32")
1848+
1849+
1850+
def test_device_memory_resource_peer_accessible_by_non_owned(mempool_device):
1851+
"""peer_accessible_by on a non-owned (default) DMR queries the driver live."""
1852+
dev = mempool_device
1853+
# The default DeviceMemoryResource(device) wraps the current device's
1854+
# default pool, i.e. _mempool_owned is False, so accessing
1855+
# peer_accessible_by exercises the live _DMR_query_peer_access path.
1856+
mr = DeviceMemoryResource(dev)
1857+
peers = mr.peer_accessible_by
1858+
assert all(isinstance(p, Device) for p in peers)
1859+
# __contains__ accepts int dev_ids; the owning device is never a peer.
1860+
assert dev.device_id not in peers
1861+
1862+
1863+
def test_dmr_mempool_get_access_self(mempool_device):
1864+
"""DMR_mempool_get_access returns 'rw' when querying the owning device itself."""
1865+
from cuda.core._memory._device_memory_resource import DMR_mempool_get_access
1866+
1867+
mr = DeviceMemoryResource(mempool_device)
1868+
# The owning device always has read-write access to its own pool.
1869+
assert DMR_mempool_get_access(mr, mempool_device.device_id) == "rw"
1870+
1871+
1872+
def test_dmr_mempool_get_access_peer(mempool_device_x2):
1873+
"""DMR_mempool_get_access reflects peer access state for a different device."""
1874+
from cuda.core._memory._device_memory_resource import DMR_mempool_get_access
1875+
1876+
dev, peer = mempool_device_x2
1877+
# Use an owned pool so peer-access state isn't contaminated by other tests
1878+
# that may have set access on the device's default pool.
1879+
mr = DeviceMemoryResource(dev, DeviceMemoryResourceOptions())
1880+
1881+
# Fresh owned pool: peer has no access.
1882+
assert DMR_mempool_get_access(mr, peer.device_id) == ""
1883+
# After granting access, peer has read-write.
1884+
mr.peer_accessible_by = [peer]
1885+
assert DMR_mempool_get_access(mr, peer.device_id) == "rw"
1886+
# After revoking, peer is back to no access.
1887+
mr.peer_accessible_by = []
1888+
assert DMR_mempool_get_access(mr, peer.device_id) == ""
1889+
1890+
1891+
def test_dmr_peer_accessible_by_setter_empty(mempool_device):
1892+
"""Assigning an empty peer-access set to a fresh owned pool is a no-op."""
1893+
mr = DeviceMemoryResource(mempool_device, options=DeviceMemoryResourceOptions())
1894+
assert set(mr.peer_accessible_by) == set()
1895+
mr.peer_accessible_by = []
1896+
assert set(mr.peer_accessible_by) == set()

cuda_core/tests/test_program.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,49 @@ def test_extra_sources_empty_source():
830830
ProgramOptions(name="test", arch="sm_80", extra_sources=[("mod", b"")])
831831

832832

833+
@pytest.mark.parametrize(
834+
("extra_sources", "expected"),
835+
[
836+
(None, None),
837+
([("mod_s", "kernel-as-string")], [(b"mod_s", b"kernel-as-string")]),
838+
(
839+
[("mod_ba", bytearray(b"\x00\x01module-as-bytearray"))],
840+
[(b"mod_ba", b"\x00\x01module-as-bytearray")],
841+
),
842+
([("mod_b", b"\x00\x01module-as-bytes")], [(b"mod_b", b"\x00\x01module-as-bytes")]),
843+
],
844+
ids=["none", "str", "bytearray", "bytes"],
845+
)
846+
def test_prepare_extra_sources_bytes(extra_sources, expected):
847+
"""_prepare_extra_sources_bytes converts each input type to (bytes, bytes) tuples (None passthrough)."""
848+
# arch is set to skip __post_init__'s Device() lookup, keeping this a pure unit test.
849+
opts = ProgramOptions(name="t", arch="sm_80", extra_sources=extra_sources)
850+
result = opts._prepare_extra_sources_bytes()
851+
assert result == expected
852+
# bytearray == bytes by content, so == alone misses type regressions.
853+
if result is not None:
854+
for name, source in result:
855+
assert isinstance(name, bytes), f"name should be bytes, got {type(name).__name__}"
856+
assert isinstance(source, bytes), f"source should be bytes, got {type(source).__name__}"
857+
858+
859+
def test_find_libdevice_path_delegates_to_pathfinder(monkeypatch):
860+
"""_find_libdevice_path calls cuda.pathfinder.find_bitcode_lib('device') and returns its result."""
861+
import cuda.pathfinder
862+
from cuda.core import _program
863+
864+
captured = []
865+
sentinel = "/fake/path/libdevice.10.bc"
866+
867+
def fake_find(name):
868+
captured.append(name)
869+
return sentinel
870+
871+
monkeypatch.setattr(cuda.pathfinder, "find_bitcode_lib", fake_find)
872+
assert _program._find_libdevice_path() == sentinel
873+
assert captured == ["device"]
874+
875+
833876
def test_nvrtc_compile_with_logs_capture(init_cuda):
834877
"""Program.compile with logs= exercises the NVRTC program-log reading path."""
835878
import io

0 commit comments

Comments
 (0)