diff --git a/cuda_core/cuda/core/_device_resources.pyi b/cuda_core/cuda/core/_device_resources.pyi index 8ae6fb6b32..7514f5a2f4 100644 --- a/cuda_core/cuda/core/_device_resources.pyi +++ b/cuda_core/cuda/core/_device_resources.pyi @@ -5,6 +5,9 @@ from __future__ import annotations from collections.abc import Sequence as SequenceABC from dataclasses import dataclass +from cuda.core._device import Device +from cuda.core.typing import WorkqueueSharingScopeType + @dataclass class SMResourceOptions: @@ -44,9 +47,9 @@ class WorkqueueResourceOptions: Attributes ---------- - sharing_scope : str, optional - Workqueue sharing scope. Accepted values: ``"device_ctx"`` - or ``"green_ctx_balanced"``. (Default to ``None``) + sharing_scope : :class:`~cuda.core.typing.WorkqueueSharingScopeType` | str, optional + Workqueue sharing scope. Accepted values: ``"device_ctx"`` or + ``"green_ctx_balanced"``. concurrency_limit : int, optional Expected maximum number of concurrent stream-ordered workloads. Must be ``>= 1`` when set. The effective @@ -55,7 +58,7 @@ class WorkqueueResourceOptions: this cap, but the driver will not guarantee that work submission remains non-overlapping. (Default to ``None``) """ - sharing_scope: str | None = None + sharing_scope: WorkqueueSharingScopeType | str | None = None concurrency_limit: int | None = None def __post_init__(self): @@ -125,6 +128,32 @@ class WorkqueueResource: def handle(self) -> int: """Return the address of the underlying config ``CUdevResource`` struct.""" + @property + def sharing_scope(self) -> WorkqueueSharingScopeType: + """Current sharing scope of this workqueue resource. + + Returns the :class:`~cuda.core.typing.WorkqueueSharingScopeType` + member corresponding to the driver-populated + ``wqConfig.sharingScope`` field. It can be updated via + :meth:`configure` with + :attr:`WorkqueueResourceOptions.sharing_scope`. + """ + + @property + def concurrency_limit(self) -> int: + """Current expected maximum concurrent stream-ordered workloads. + + Reflects the driver-populated ``wqConfig.wqConcurrencyLimit`` field. + When first queried from a device, this matches the driver-reported + cap (typically ``CUDA_DEVICE_MAX_CONNECTIONS``). It can be updated + via :meth:`configure` with + :attr:`WorkqueueResourceOptions.concurrency_limit`. + """ + + @property + def device(self) -> Device: + """The :class:`~cuda.core.Device` this workqueue resource is available on.""" + def configure(self, options: WorkqueueResourceOptions) -> None: """Configure the workqueue resource in place. diff --git a/cuda_core/cuda/core/_device_resources.pyx b/cuda_core/cuda/core/_device_resources.pyx index 1e1f016f6c..0c01956d2e 100644 --- a/cuda_core/cuda/core/_device_resources.pyx +++ b/cuda_core/cuda/core/_device_resources.pyx @@ -6,6 +6,11 @@ from __future__ import annotations from collections.abc import Sequence as SequenceABC from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from cuda.core._device import Device + from cuda.core.typing import WorkqueueSharingScopeType from libc.stdint cimport intptr_t from libc.stdlib cimport free, malloc @@ -126,9 +131,9 @@ cdef class WorkqueueResourceOptions: Attributes ---------- - sharing_scope : str, optional - Workqueue sharing scope. Accepted values: ``"device_ctx"`` - or ``"green_ctx_balanced"``. (Default to ``None``) + sharing_scope : :class:`~cuda.core.typing.WorkqueueSharingScopeType` | str, optional + Workqueue sharing scope. Accepted values: ``"device_ctx"`` or + ``"green_ctx_balanced"``. concurrency_limit : int, optional Expected maximum number of concurrent stream-ordered workloads. Must be ``>= 1`` when set. The effective @@ -138,7 +143,7 @@ cdef class WorkqueueResourceOptions: submission remains non-overlapping. (Default to ``None``) """ - sharing_scope: str | None = None + sharing_scope: WorkqueueSharingScopeType | str | None = None concurrency_limit: int | None = None def __post_init__(self): @@ -554,6 +559,57 @@ cdef class WorkqueueResource: """Return the address of the underlying config ``CUdevResource`` struct.""" return (&self._wq_config_resource) + @property + def sharing_scope(self) -> WorkqueueSharingScopeType: + """Current sharing scope of this workqueue resource. + + Returns the :class:`~cuda.core.typing.WorkqueueSharingScopeType` + member corresponding to the driver-populated + ``wqConfig.sharingScope`` field. It can be updated via + :meth:`configure` with + :attr:`WorkqueueResourceOptions.sharing_scope`. + """ + IF CUDA_CORE_BUILD_MAJOR >= 13: + from cuda.core.typing import WorkqueueSharingScopeType + cdef object scope = self._wq_config_resource.wqConfig.sharingScope + if scope == cydriver.CUdevWorkqueueConfigScope.CU_WORKQUEUE_SCOPE_DEVICE_CTX: + return WorkqueueSharingScopeType.DEVICE_CTX + elif scope == cydriver.CUdevWorkqueueConfigScope.CU_WORKQUEUE_SCOPE_GREEN_CTX_BALANCED: + return WorkqueueSharingScopeType.GREEN_CTX_BALANCED + raise RuntimeError(f"Unknown sharing scope enum value: {scope}") + ELSE: + raise RuntimeError( + "WorkqueueResource requires cuda.core to be built with CUDA 13.x bindings" + ) + + @property + def concurrency_limit(self) -> int: + """Current expected maximum concurrent stream-ordered workloads. + + Reflects the driver-populated ``wqConfig.wqConcurrencyLimit`` field. + When first queried from a device, this matches the driver-reported + cap (typically ``CUDA_DEVICE_MAX_CONNECTIONS``). It can be updated + via :meth:`configure` with + :attr:`WorkqueueResourceOptions.concurrency_limit`. + """ + IF CUDA_CORE_BUILD_MAJOR >= 13: + return self._wq_config_resource.wqConfig.wqConcurrencyLimit + ELSE: + raise RuntimeError( + "WorkqueueResource requires cuda.core to be built with CUDA 13.x bindings" + ) + + @property + def device(self) -> Device: + """The :class:`~cuda.core.Device` this workqueue resource is available on.""" + IF CUDA_CORE_BUILD_MAJOR >= 13: + from cuda.core._device import Device # avoid circular import + return Device(int(self._wq_config_resource.wqConfig.device)) + ELSE: + raise RuntimeError( + "WorkqueueResource requires cuda.core to be built with CUDA 13.x bindings" + ) + def configure(self, options: WorkqueueResourceOptions) -> None: """Configure the workqueue resource in place. diff --git a/cuda_core/cuda/core/typing.py b/cuda_core/cuda/core/typing.py index 2d253d8ca5..1bf9bb7c0d 100644 --- a/cuda_core/cuda/core/typing.py +++ b/cuda_core/cuda/core/typing.py @@ -52,6 +52,7 @@ class StrEnum(str, Enum): "VirtualMemoryGranularityType", "VirtualMemoryHandleType", "VirtualMemoryLocationType", + "WorkqueueSharingScopeType", ] @@ -296,4 +297,19 @@ class ReadModeType(StrEnum): NORMALIZED_FLOAT = "normalized_float" +class WorkqueueSharingScopeType(StrEnum): + """Sharing scope for :class:`~cuda.core.WorkqueueResource`. + + * ``DEVICE_CTX`` — use all shared workqueue resources across all + contexts (default driver behavior). + * ``GREEN_CTX_BALANCED`` — when possible, use non-overlapping + workqueue resources with other balanced green contexts. + + .. versionadded:: 1.1.0 + """ + + DEVICE_CTX = "device_ctx" + GREEN_CTX_BALANCED = "green_ctx_balanced" + + del StrEnum diff --git a/cuda_core/docs/source/release/1.1.0-notes.rst b/cuda_core/docs/source/release/1.1.0-notes.rst index d9fe0a7a5b..2a28290498 100644 --- a/cuda_core/docs/source/release/1.1.0-notes.rst +++ b/cuda_core/docs/source/release/1.1.0-notes.rst @@ -106,9 +106,19 @@ New features (`#2068 `__, closes `#2049 `__) -- Added ``concurrency_limit`` to :class:`WorkqueueResourceOptions` to - configure the expected maximum concurrent stream-ordered workloads. - (`#2329 `__) +- Extended :class:`WorkqueueResource` and :class:`WorkqueueResourceOptions` + to cover the full driver-side workqueue-config surface. Added + ``concurrency_limit`` to :class:`WorkqueueResourceOptions` for + configuring the expected maximum concurrent stream-ordered workloads, + and read-only :attr:`WorkqueueResource.sharing_scope`, + :attr:`~WorkqueueResource.concurrency_limit`, and + :attr:`~WorkqueueResource.device` properties for round-tripping the + driver-populated values. Added + :class:`~cuda.core.typing.WorkqueueSharingScopeType` StrEnum accepted + by :attr:`WorkqueueResourceOptions.sharing_scope` in addition to raw + strings. + (`#2329 `__, + `#2330 `__) Fixes and enhancements ---------------------- diff --git a/cuda_core/tests/test_enum_coverage.py b/cuda_core/tests/test_enum_coverage.py index 09c12bbaf5..8de26b25d4 100644 --- a/cuda_core/tests/test_enum_coverage.py +++ b/cuda_core/tests/test_enum_coverage.py @@ -310,6 +310,24 @@ } +# CUdevWorkqueueConfigScope was added to the CUDA driver in 13.1 (missing +# from the 13.0.0 cuda.h and earlier); on cuda-bindings for CUDA 12.x or +# 13.0.x, WorkqueueSharingScopeType has no driver-side counterpart to +# check against. +if hasattr(driver, "CUdevWorkqueueConfigScope"): + _CASES.append( + ( + driver.CUdevWorkqueueConfigScope, + cuda.core.typing.WorkqueueSharingScopeType, + None, + set(), + set(), + ) + ) +else: + _UNBOUND_STR_ENUMS.add(cuda.core.typing.WorkqueueSharingScopeType) + + @pytest.mark.parametrize( "binding, str_enum, mapping, binding_unmapped, str_enum_unmapped", _CASES, diff --git a/cuda_core/tests/test_green_context.py b/cuda_core/tests/test_green_context.py index ee3896c53e..effa716737 100644 --- a/cuda_core/tests/test_green_context.py +++ b/cuda_core/tests/test_green_context.py @@ -22,6 +22,7 @@ launch, ) from cuda.core._utils.cuda_utils import CUDAError +from cuda.core.typing import WorkqueueSharingScopeType # --------------------------------------------------------------------------- # Kernel source @@ -42,12 +43,23 @@ # --------------------------------------------------------------------------- +# Resource queries (dev.resources.sm, dev.resources.workqueue) can fail in +# three orthogonal ways when the resource type isn't supported here: +# * RuntimeError — cuda.core was built against CUDA bindings that don't +# expose the resource (e.g. WorkqueueResource on 12.x). +# * ValueError — the runtime driver version is too old to support the +# resource (green-context / workqueue support gates). +# * CUDAError — the driver rejected the specific device-level query. +# Skip on any of them. +_RESOURCE_UNAVAILABLE_ERRORS = (RuntimeError, ValueError, CUDAError) + + @pytest.fixture def sm_resource(init_cuda): """Query SM resources from the device, skip if unsupported.""" try: return init_cuda.resources.sm - except (RuntimeError, ValueError, CUDAError) as exc: + except _RESOURCE_UNAVAILABLE_ERRORS as exc: pytest.skip(str(exc)) @@ -56,7 +68,7 @@ def wq_resource(init_cuda): """Query workqueue resources from the device, skip if unsupported.""" try: return init_cuda.resources.workqueue - except (RuntimeError, ValueError, CUDAError) as exc: + except _RESOURCE_UNAVAILABLE_ERRORS as exc: pytest.skip(str(exc)) @@ -207,8 +219,11 @@ def test_arch_constraints_hopper_plus(self, init_cuda, sm_resource): class TestWorkqueueResource: - def test_query(self, wq_resource): + def test_query(self, init_cuda, wq_resource): assert wq_resource.handle != 0 + assert isinstance(wq_resource.sharing_scope, WorkqueueSharingScopeType) + assert wq_resource.concurrency_limit >= 1 + assert wq_resource.device.device_id == init_cuda.device_id def test_configure_none_is_noop(self, wq_resource): assert wq_resource.configure(WorkqueueResourceOptions(sharing_scope=None)) is None @@ -216,12 +231,33 @@ def test_configure_none_is_noop(self, wq_resource): def test_configure_valid_scope(self, wq_resource): wq_resource.configure(WorkqueueResourceOptions(sharing_scope="green_ctx_balanced")) + @pytest.mark.parametrize("scope", list(WorkqueueSharingScopeType)) + def test_configure_scope_with_enum(self, wq_resource, scope): + wq_resource.configure(WorkqueueResourceOptions(sharing_scope=scope)) + assert wq_resource.sharing_scope is scope + + def test_device_id_matches_source_multi_gpu(self): + from cuda.core import Device, system + + if system.get_num_devices() < 2: + pytest.skip("requires 2+ GPUs") + dev0 = Device(0) + dev1 = Device(1) + try: + wq0 = dev0.resources.workqueue + wq1 = dev1.resources.workqueue + except _RESOURCE_UNAVAILABLE_ERRORS as exc: + pytest.skip(str(exc)) + assert wq0.device.device_id == 0 + assert wq1.device.device_id == 1 + def test_invalid_scope_raises_at_construction(self): with pytest.raises(ValueError, match="Unknown sharing_scope"): WorkqueueResourceOptions(sharing_scope="bogus") def test_configure_concurrency_limit(self, wq_resource): wq_resource.configure(WorkqueueResourceOptions(concurrency_limit=4)) + assert wq_resource.concurrency_limit == 4 def test_configure_concurrency_and_scope(self, wq_resource): wq_resource.configure(