diff --git a/cuda_core/cuda/core/_device_resources.pyx b/cuda_core/cuda/core/_device_resources.pyx index 0c01956d2ea..26c8863e063 100644 --- a/cuda_core/cuda/core/_device_resources.pyx +++ b/cuda_core/cuda/core/_device_resources.pyx @@ -21,6 +21,7 @@ from cuda.core._resource_handles cimport ContextHandle, GreenCtxHandle, as_cu, g from cuda.core._utils.cuda_utils cimport check_or_create_options, HANDLE_RETURN from cuda.core._utils.cuda_utils import is_sequence from cuda.core._utils.version cimport cy_binding_version, cy_driver_version +from cuda.core._utils.validators import check_str_enum __all__ = [ @@ -147,11 +148,8 @@ cdef class WorkqueueResourceOptions: concurrency_limit: int | None = None def __post_init__(self): - if self.sharing_scope not in (None, "device_ctx", "green_ctx_balanced"): - raise ValueError( - f"Unknown sharing_scope: {self.sharing_scope!r}. " - "Expected 'device_ctx' or 'green_ctx_balanced'." - ) + from cuda.core.typing import WorkqueueSharingScopeType + check_str_enum(self.sharing_scope, WorkqueueSharingScopeType, allow_none=True) if self.concurrency_limit is not None and self.concurrency_limit < 1: raise ValueError( f"concurrency_limit must be >= 1, got {self.concurrency_limit}" diff --git a/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx b/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx index 41b7a992bed..d5a637a7b50 100644 --- a/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx +++ b/cuda_core/cuda/core/_memory/_managed_memory_resource.pyx @@ -21,6 +21,9 @@ import warnings from cuda.core._memory._managed_buffer import ManagedBuffer from cuda.core.typing import ManagedMemoryLocationType +IF CUDA_CORE_BUILD_MAJOR >= 13: + from cuda.core._utils.validators import check_str_enum + if TYPE_CHECKING: from cuda.core.graph import GraphBuilder @@ -163,9 +166,6 @@ cdef class ManagedMemoryResource(_MemPool): IF CUDA_CORE_BUILD_MAJOR >= 13: - cdef tuple _VALID_LOCATION_TYPES = ("device", "host", "host_numa") - - cdef _resolve_preferred_location(ManagedMemoryResourceOptions opts): """Resolve preferred location options into driver and stored values. @@ -175,11 +175,7 @@ IF CUDA_CORE_BUILD_MAJOR >= 13: cdef object pref_loc = opts.preferred_location if opts is not None else None cdef object pref_type = opts.preferred_location_type if opts is not None else None - if pref_type is not None and pref_type not in _VALID_LOCATION_TYPES: - raise ValueError( - f"preferred_location_type must be one of {_VALID_LOCATION_TYPES!r} " - f"or None, got {pref_type!r}" - ) + check_str_enum(pref_type, ManagedMemoryLocationType, allow_none=True) if pref_type is None: # Legacy behavior diff --git a/cuda_core/cuda/core/_utils/validators.py b/cuda_core/cuda/core/_utils/validators.py new file mode 100644 index 00000000000..dac3d1c788e --- /dev/null +++ b/cuda_core/cuda/core/_utils/validators.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + + +def format_or_list(values): + """Format an iterable of values as a human-readable ``A, B or C`` string. + + Each value is passed through :func:`repr`. A single value is returned + as-is; two values are joined with ``" or "``; three or more use a + comma-separated list with ``" or "`` before the last item. + """ + reprs = [repr(v) for v in values] + if len(reprs) <= 1: + return reprs[0] if reprs else "" + *head, tail = reprs + return ", ".join(head) + " or " + tail + + +def check_str_enum(value, enum_class, *, allow_none=False): + """Raise ValueError if *value* is not a member of *enum_class*. + + Derives the list of acceptable values from the enum itself so callers + do not need to maintain a parallel copy of the valid strings. + + If *allow_none* is True, ``None`` is also accepted and included in the + error message when an invalid value is provided. + """ + if allow_none and value is None: + return + if value not in {m.value for m in enum_class}: + valid = sorted(m.value for m in enum_class) + if allow_none: + valid = [None, *valid] + raise ValueError(f"{value!r} is not a valid {enum_class.__name__}. Must be {format_or_list(valid)}") diff --git a/cuda_core/cuda/core/graph/_graph_node.pyx b/cuda_core/cuda/core/graph/_graph_node.pyx index 077eff77ea0..0adf69ad4b4 100644 --- a/cuda_core/cuda/core/graph/_graph_node.pyx +++ b/cuda_core/cuda/core/graph/_graph_node.pyx @@ -65,6 +65,7 @@ import ctypes as ct import weakref from cuda.core.graph._adjacency_set_proxy import AdjacencySetProxy +from cuda.core._utils.validators import check_str_enum from cuda.core._utils.cuda_utils import driver from cuda.core.typing import GraphMemoryType @@ -787,6 +788,7 @@ cdef inline AllocNode GN_alloc(GraphNode self, size_t size, object device, alloc_params.poolProps.handleTypes = cydriver.CUmemAllocationHandleType.CU_MEM_HANDLE_TYPE_NONE alloc_params.bytesize = size + check_str_enum(memory_type_str, GraphMemoryType) if memory_type_str == "device": alloc_params.poolProps.allocType = cydriver.CUmemAllocationType.CU_MEM_ALLOCATION_TYPE_PINNED alloc_params.poolProps.location.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE @@ -802,9 +804,6 @@ cdef inline AllocNode GN_alloc(GraphNode self, size_t size, object device, alloc_params.poolProps.location.id = device_id ELSE: raise ValueError("memory_type='managed' requires CUDA 13.0 or later") - else: - raise ValueError(f"Invalid memory_type: {memory_type_str!r}. " - "Must be 'device', 'host', or 'managed'.") if access_descs.size() > 0: alloc_params.accessDescs = access_descs.data() diff --git a/cuda_core/cuda/core/utils/_program_cache/_keys.py b/cuda_core/cuda/core/utils/_program_cache/_keys.py index 668a26257fb..e170bc18131 100644 --- a/cuda_core/cuda/core/utils/_program_cache/_keys.py +++ b/cuda_core/cuda/core/utils/_program_cache/_keys.py @@ -33,11 +33,12 @@ from cuda.core._utils.cuda_utils import ( nvrtc as _nvrtc, ) +from cuda.core._utils.validators import check_str_enum, format_or_list +from cuda.core.typing import SourceCodeType # Bump when the key schema changes in a way that invalidates existing caches. _KEY_SCHEMA_VERSION = 2 -_VALID_CODE_TYPES = frozenset({"c++", "ptx", "nvvm"}) _VALID_TARGET_TYPES = frozenset({"ptx", "cubin", "ltoir"}) # code_type -> allowed target_type set, mirroring Program.compile's @@ -752,10 +753,12 @@ def make_program_cache_key( # the lowercase form. code_type = code_type.lower() if isinstance(code_type, str) else code_type target_type = target_type.lower() if isinstance(target_type, str) else target_type - if code_type not in _VALID_CODE_TYPES: - raise ValueError(f"code_type={code_type!r} is not supported (must be one of {sorted(_VALID_CODE_TYPES)})") + check_str_enum(code_type, SourceCodeType) if target_type not in _VALID_TARGET_TYPES: - raise ValueError(f"target_type={target_type!r} is not supported (must be one of {sorted(_VALID_TARGET_TYPES)})") + raise ValueError( + f"target_type={target_type!r} is not supported by the program cache " + f"(must be {format_or_list(sorted(_VALID_TARGET_TYPES))})" + ) supported_for_code = _SUPPORTED_TARGETS_BY_CODE_TYPE[code_type] if target_type not in supported_for_code: raise ValueError( diff --git a/cuda_core/tests/graph/test_graph_definition.py b/cuda_core/tests/graph/test_graph_definition.py index da78bea577f..50d4b4ac253 100644 --- a/cuda_core/tests/graph/test_graph_definition.py +++ b/cuda_core/tests/graph/test_graph_definition.py @@ -833,7 +833,7 @@ def test_alloc_free_chain(sample_graphdef): def test_alloc_memory_type_invalid(sample_graphdef): """Invalid memory type raises ValueError.""" - with pytest.raises(ValueError, match="Invalid memory_type"): + with pytest.raises(ValueError, match="'invalid' is not a valid GraphMemoryType. Must be "): sample_graphdef.allocate(ALLOC_SIZE, memory_type="invalid") diff --git a/cuda_core/tests/test_green_context.py b/cuda_core/tests/test_green_context.py index effa7167371..999627a901a 100644 --- a/cuda_core/tests/test_green_context.py +++ b/cuda_core/tests/test_green_context.py @@ -252,7 +252,7 @@ def test_device_id_matches_source_multi_gpu(self): assert wq1.device.device_id == 1 def test_invalid_scope_raises_at_construction(self): - with pytest.raises(ValueError, match="Unknown sharing_scope"): + with pytest.raises(ValueError, match="'bogus' is not a valid WorkqueueSharingScopeType. Must be "): WorkqueueResourceOptions(sharing_scope="bogus") def test_configure_concurrency_limit(self, wq_resource): diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index f53e8244328..5ef48919a50 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -1278,7 +1278,7 @@ def test_managed_memory_resource_preferred_location_validation(init_cuda): device.set_current() # Invalid preferred_location_type - with pytest.raises(ValueError, match="preferred_location_type must be one of"): + with pytest.raises(ValueError, match="'invalid' is not a valid ManagedMemoryLocationType. Must be "): ManagedMemoryResource( ManagedMemoryResourceOptions( preferred_location_type="invalid", diff --git a/cuda_core/tests/test_program_cache.py b/cuda_core/tests/test_program_cache.py index 963ec1cc04b..ee393868640 100644 --- a/cuda_core/tests/test_program_cache.py +++ b/cuda_core/tests/test_program_cache.py @@ -317,7 +317,9 @@ def test_make_program_cache_key_rejects_extra_sources_outside_nvvm(code_type, co @pytest.mark.parametrize( "kwargs, exc_type, match", [ - pytest.param({"code_type": "fortran"}, ValueError, "code_type", id="unknown_code_type"), + pytest.param( + {"code_type": "fortran"}, ValueError, "'fortran' is not a valid SourceCodeType", id="unknown_code_type" + ), pytest.param({"target_type": "exe"}, ValueError, "target_type", id="unknown_target_type"), pytest.param({"code": 12345}, TypeError, "code", id="non_str_bytes_code"), # Backend-specific target matrix -- Program.compile rejects these