Skip to content

Commit 52043f9

Browse files
leofangmdboom
andauthored
cuda.core: expose driver-populated fields on WorkqueueResource (#2330)
* cuda.core: expose driver-populated fields on WorkqueueResource Add read-only sharing_scope, concurrency_limit, and device properties on WorkqueueResource so users can inspect the driver-populated config without dropping to raw cydriver. Add WorkqueueSharingScopeType StrEnum in cuda.core.typing to give the sharing scope a typed return; strings matching the enum member values remain accepted for backward compat. Follows section 5(d) "opaque but round-trippable" from the green ctx design doc, which was written into SMResource but not applied to WorkqueueResource. * cuda.core: fill in PR number in release-notes entry * cuda.core: drop TYPE_CHECKING Device import to satisfy cython-lint The lazy import inside WorkqueueResource.device already imports Device at call time; the top-level TYPE_CHECKING import was unused as far as cython-lint could see and blocked pre-commit. * cuda.core: fold #2329 + #2330 release notes; parametrize enum test; add 2-GPU device check - Merge the two 1.1.0 workqueue entries into one bullet crediting both PRs. - Parametrize test_configure_scope_with_enum over both WorkqueueSharingScopeType members. - Add test_device_id_matches_source_multi_gpu verifying WorkqueueResource.device.device_id tracks the source device when the caller switches devices. Uses the established system.get_num_devices() < 2 skip pattern. * cuda.core tests: drop init_cuda from multi-GPU workqueue test * cuda.core tests: drop unnecessary set_current calls in multi-GPU workqueue test cuDeviceGetDevResource doesn't require the device to be current; verified locally that cuCtxGetCurrent returns the same context before and after the test body. * cuda.core tests: extract shared _RESOURCE_UNAVAILABLE_ERRORS tuple with rationale * cuda.core: restore Device TYPE_CHECKING import with noqa mypy needs Device resolvable in the generated .pyi (WorkqueueResource.device returns 'Device' as a string forward reference); noqa: F401 keeps cython-lint from re-flagging it as unused. * cuda.core: simplify noqa to bare form (cython-lint didn't parse the parenthetical) * cuda.core: use bare Device annotation on WorkqueueResource.device cython-lint doesn't count string-quoted forward references as usage of the TYPE_CHECKING import. The bare form works because from __future__ import annotations makes it a string at runtime anyway (matches _stream.pyx pattern). * cuda.core tests: register WorkqueueSharingScopeType in _CASES test_all_str_enums_in_cases enforces that every StrEnum in cuda.core.typing is bound to a driver-side counterpart or explicitly marked unbound. WorkqueueSharingScopeType wraps CUdevWorkqueueConfigScope 1:1 — clean binding-coverage entry. * cuda.core tests: gate WorkqueueSharingScopeType binding entry on CUDA 13+ CUdevWorkqueueConfigScope doesn't exist in CUDA 12.x cuda_bindings, so importing test_enum_coverage crashed with AttributeError. Only register the binding-coverage entry when the driver exposes the enum; otherwise mark the wrapper as unbound so test_all_str_enums_in_cases still passes. Verified both paths locally (real CUDA 13 + delattr-simulated CUDA 12). * cuda.core tests: sharpen CUDA-version boundary in WorkqueueSharingScopeType comment CUdevWorkqueueConfigScope landed in the driver in 13.1, not 13.0. Update the inline comment on the hasattr gate to reflect that the missing-driver-enum path covers cuda-bindings for both CUDA 12.x and CUDA 13.0.x. * cuda.core: apply Mike's suggestion for WorkqueueResourceOptions.sharing_scope docstring * fix typing Co-authored-by: Michael Droettboom <mdboom@gmail.com> * Update _device_resources.pyi * cuda.core: regenerate .pyi to match stubgen-pyx output end-of-file-fixer excludes .pyi (per .pre-commit-config.yaml), so the trailing newline manually added in the previous commit made stubgen-pyx flag the file every run. Reverting to the newline-less form stubgen emits. * Apply suggestions from code review Co-authored-by: Michael Droettboom <mdboom@gmail.com> --------- Co-authored-by: Michael Droettboom <mdboom@gmail.com>
1 parent e1f5d97 commit 52043f9

6 files changed

Lines changed: 179 additions & 14 deletions

File tree

cuda_core/cuda/core/_device_resources.pyi

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ from __future__ import annotations
55
from collections.abc import Sequence as SequenceABC
66
from dataclasses import dataclass
77

8+
from cuda.core._device import Device
9+
from cuda.core.typing import WorkqueueSharingScopeType
10+
811

912
@dataclass
1013
class SMResourceOptions:
@@ -44,9 +47,9 @@ class WorkqueueResourceOptions:
4447
4548
Attributes
4649
----------
47-
sharing_scope : str, optional
48-
Workqueue sharing scope. Accepted values: ``"device_ctx"``
49-
or ``"green_ctx_balanced"``. (Default to ``None``)
50+
sharing_scope : :class:`~cuda.core.typing.WorkqueueSharingScopeType` | str, optional
51+
Workqueue sharing scope. Accepted values: ``"device_ctx"`` or
52+
``"green_ctx_balanced"``.
5053
concurrency_limit : int, optional
5154
Expected maximum number of concurrent stream-ordered
5255
workloads. Must be ``>= 1`` when set. The effective
@@ -55,7 +58,7 @@ class WorkqueueResourceOptions:
5558
this cap, but the driver will not guarantee that work
5659
submission remains non-overlapping. (Default to ``None``)
5760
"""
58-
sharing_scope: str | None = None
61+
sharing_scope: WorkqueueSharingScopeType | str | None = None
5962
concurrency_limit: int | None = None
6063

6164
def __post_init__(self):
@@ -125,6 +128,32 @@ class WorkqueueResource:
125128
def handle(self) -> int:
126129
"""Return the address of the underlying config ``CUdevResource`` struct."""
127130

131+
@property
132+
def sharing_scope(self) -> WorkqueueSharingScopeType:
133+
"""Current sharing scope of this workqueue resource.
134+
135+
Returns the :class:`~cuda.core.typing.WorkqueueSharingScopeType`
136+
member corresponding to the driver-populated
137+
``wqConfig.sharingScope`` field. It can be updated via
138+
:meth:`configure` with
139+
:attr:`WorkqueueResourceOptions.sharing_scope`.
140+
"""
141+
142+
@property
143+
def concurrency_limit(self) -> int:
144+
"""Current expected maximum concurrent stream-ordered workloads.
145+
146+
Reflects the driver-populated ``wqConfig.wqConcurrencyLimit`` field.
147+
When first queried from a device, this matches the driver-reported
148+
cap (typically ``CUDA_DEVICE_MAX_CONNECTIONS``). It can be updated
149+
via :meth:`configure` with
150+
:attr:`WorkqueueResourceOptions.concurrency_limit`.
151+
"""
152+
153+
@property
154+
def device(self) -> Device:
155+
"""The :class:`~cuda.core.Device` this workqueue resource is available on."""
156+
128157
def configure(self, options: WorkqueueResourceOptions) -> None:
129158
"""Configure the workqueue resource in place.
130159

cuda_core/cuda/core/_device_resources.pyx

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ from __future__ import annotations
66

77
from collections.abc import Sequence as SequenceABC
88
from dataclasses import dataclass
9+
from typing import TYPE_CHECKING
10+
11+
if TYPE_CHECKING:
12+
from cuda.core._device import Device
13+
from cuda.core.typing import WorkqueueSharingScopeType
914

1015
from libc.stdint cimport intptr_t
1116
from libc.stdlib cimport free, malloc
@@ -126,9 +131,9 @@ cdef class WorkqueueResourceOptions:
126131
127132
Attributes
128133
----------
129-
sharing_scope : str, optional
130-
Workqueue sharing scope. Accepted values: ``"device_ctx"``
131-
or ``"green_ctx_balanced"``. (Default to ``None``)
134+
sharing_scope : :class:`~cuda.core.typing.WorkqueueSharingScopeType` | str, optional
135+
Workqueue sharing scope. Accepted values: ``"device_ctx"`` or
136+
``"green_ctx_balanced"``.
132137
concurrency_limit : int, optional
133138
Expected maximum number of concurrent stream-ordered
134139
workloads. Must be ``>= 1`` when set. The effective
@@ -138,7 +143,7 @@ cdef class WorkqueueResourceOptions:
138143
submission remains non-overlapping. (Default to ``None``)
139144
"""
140145

141-
sharing_scope: str | None = None
146+
sharing_scope: WorkqueueSharingScopeType | str | None = None
142147
concurrency_limit: int | None = None
143148

144149
def __post_init__(self):
@@ -554,6 +559,57 @@ cdef class WorkqueueResource:
554559
"""Return the address of the underlying config ``CUdevResource`` struct."""
555560
return <intptr_t>(&self._wq_config_resource)
556561

562+
@property
563+
def sharing_scope(self) -> WorkqueueSharingScopeType:
564+
"""Current sharing scope of this workqueue resource.
565+
566+
Returns the :class:`~cuda.core.typing.WorkqueueSharingScopeType`
567+
member corresponding to the driver-populated
568+
``wqConfig.sharingScope`` field. It can be updated via
569+
:meth:`configure` with
570+
:attr:`WorkqueueResourceOptions.sharing_scope`.
571+
"""
572+
IF CUDA_CORE_BUILD_MAJOR >= 13:
573+
from cuda.core.typing import WorkqueueSharingScopeType
574+
cdef object scope = self._wq_config_resource.wqConfig.sharingScope
575+
if scope == cydriver.CUdevWorkqueueConfigScope.CU_WORKQUEUE_SCOPE_DEVICE_CTX:
576+
return WorkqueueSharingScopeType.DEVICE_CTX
577+
elif scope == cydriver.CUdevWorkqueueConfigScope.CU_WORKQUEUE_SCOPE_GREEN_CTX_BALANCED:
578+
return WorkqueueSharingScopeType.GREEN_CTX_BALANCED
579+
raise RuntimeError(f"Unknown sharing scope enum value: {scope}")
580+
ELSE:
581+
raise RuntimeError(
582+
"WorkqueueResource requires cuda.core to be built with CUDA 13.x bindings"
583+
)
584+
585+
@property
586+
def concurrency_limit(self) -> int:
587+
"""Current expected maximum concurrent stream-ordered workloads.
588+
589+
Reflects the driver-populated ``wqConfig.wqConcurrencyLimit`` field.
590+
When first queried from a device, this matches the driver-reported
591+
cap (typically ``CUDA_DEVICE_MAX_CONNECTIONS``). It can be updated
592+
via :meth:`configure` with
593+
:attr:`WorkqueueResourceOptions.concurrency_limit`.
594+
"""
595+
IF CUDA_CORE_BUILD_MAJOR >= 13:
596+
return self._wq_config_resource.wqConfig.wqConcurrencyLimit
597+
ELSE:
598+
raise RuntimeError(
599+
"WorkqueueResource requires cuda.core to be built with CUDA 13.x bindings"
600+
)
601+
602+
@property
603+
def device(self) -> Device:
604+
"""The :class:`~cuda.core.Device` this workqueue resource is available on."""
605+
IF CUDA_CORE_BUILD_MAJOR >= 13:
606+
from cuda.core._device import Device # avoid circular import
607+
return Device(int(self._wq_config_resource.wqConfig.device))
608+
ELSE:
609+
raise RuntimeError(
610+
"WorkqueueResource requires cuda.core to be built with CUDA 13.x bindings"
611+
)
612+
557613
def configure(self, options: WorkqueueResourceOptions) -> None:
558614
"""Configure the workqueue resource in place.
559615

cuda_core/cuda/core/typing.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class StrEnum(str, Enum):
5252
"VirtualMemoryGranularityType",
5353
"VirtualMemoryHandleType",
5454
"VirtualMemoryLocationType",
55+
"WorkqueueSharingScopeType",
5556
]
5657

5758

@@ -296,4 +297,19 @@ class ReadModeType(StrEnum):
296297
NORMALIZED_FLOAT = "normalized_float"
297298

298299

300+
class WorkqueueSharingScopeType(StrEnum):
301+
"""Sharing scope for :class:`~cuda.core.WorkqueueResource`.
302+
303+
* ``DEVICE_CTX`` — use all shared workqueue resources across all
304+
contexts (default driver behavior).
305+
* ``GREEN_CTX_BALANCED`` — when possible, use non-overlapping
306+
workqueue resources with other balanced green contexts.
307+
308+
.. versionadded:: 1.1.0
309+
"""
310+
311+
DEVICE_CTX = "device_ctx"
312+
GREEN_CTX_BALANCED = "green_ctx_balanced"
313+
314+
299315
del StrEnum

cuda_core/docs/source/release/1.1.0-notes.rst

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,19 @@ New features
106106
(`#2068 <https://github.com/NVIDIA/cuda-python/pull/2068>`__,
107107
closes `#2049 <https://github.com/NVIDIA/cuda-python/issues/2049>`__)
108108

109-
- Added ``concurrency_limit`` to :class:`WorkqueueResourceOptions` to
110-
configure the expected maximum concurrent stream-ordered workloads.
111-
(`#2329 <https://github.com/NVIDIA/cuda-python/pull/2329>`__)
109+
- Extended :class:`WorkqueueResource` and :class:`WorkqueueResourceOptions`
110+
to cover the full driver-side workqueue-config surface. Added
111+
``concurrency_limit`` to :class:`WorkqueueResourceOptions` for
112+
configuring the expected maximum concurrent stream-ordered workloads,
113+
and read-only :attr:`WorkqueueResource.sharing_scope`,
114+
:attr:`~WorkqueueResource.concurrency_limit`, and
115+
:attr:`~WorkqueueResource.device` properties for round-tripping the
116+
driver-populated values. Added
117+
:class:`~cuda.core.typing.WorkqueueSharingScopeType` StrEnum accepted
118+
by :attr:`WorkqueueResourceOptions.sharing_scope` in addition to raw
119+
strings.
120+
(`#2329 <https://github.com/NVIDIA/cuda-python/pull/2329>`__,
121+
`#2330 <https://github.com/NVIDIA/cuda-python/pull/2330>`__)
112122

113123
Fixes and enhancements
114124
----------------------

cuda_core/tests/test_enum_coverage.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,24 @@
310310
}
311311

312312

313+
# CUdevWorkqueueConfigScope was added to the CUDA driver in 13.1 (missing
314+
# from the 13.0.0 cuda.h and earlier); on cuda-bindings for CUDA 12.x or
315+
# 13.0.x, WorkqueueSharingScopeType has no driver-side counterpart to
316+
# check against.
317+
if hasattr(driver, "CUdevWorkqueueConfigScope"):
318+
_CASES.append(
319+
(
320+
driver.CUdevWorkqueueConfigScope,
321+
cuda.core.typing.WorkqueueSharingScopeType,
322+
None,
323+
set(),
324+
set(),
325+
)
326+
)
327+
else:
328+
_UNBOUND_STR_ENUMS.add(cuda.core.typing.WorkqueueSharingScopeType)
329+
330+
313331
@pytest.mark.parametrize(
314332
"binding, str_enum, mapping, binding_unmapped, str_enum_unmapped",
315333
_CASES,

cuda_core/tests/test_green_context.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
launch,
2323
)
2424
from cuda.core._utils.cuda_utils import CUDAError
25+
from cuda.core.typing import WorkqueueSharingScopeType
2526

2627
# ---------------------------------------------------------------------------
2728
# Kernel source
@@ -42,12 +43,23 @@
4243
# ---------------------------------------------------------------------------
4344

4445

46+
# Resource queries (dev.resources.sm, dev.resources.workqueue) can fail in
47+
# three orthogonal ways when the resource type isn't supported here:
48+
# * RuntimeError — cuda.core was built against CUDA bindings that don't
49+
# expose the resource (e.g. WorkqueueResource on 12.x).
50+
# * ValueError — the runtime driver version is too old to support the
51+
# resource (green-context / workqueue support gates).
52+
# * CUDAError — the driver rejected the specific device-level query.
53+
# Skip on any of them.
54+
_RESOURCE_UNAVAILABLE_ERRORS = (RuntimeError, ValueError, CUDAError)
55+
56+
4557
@pytest.fixture
4658
def sm_resource(init_cuda):
4759
"""Query SM resources from the device, skip if unsupported."""
4860
try:
4961
return init_cuda.resources.sm
50-
except (RuntimeError, ValueError, CUDAError) as exc:
62+
except _RESOURCE_UNAVAILABLE_ERRORS as exc:
5163
pytest.skip(str(exc))
5264

5365

@@ -56,7 +68,7 @@ def wq_resource(init_cuda):
5668
"""Query workqueue resources from the device, skip if unsupported."""
5769
try:
5870
return init_cuda.resources.workqueue
59-
except (RuntimeError, ValueError, CUDAError) as exc:
71+
except _RESOURCE_UNAVAILABLE_ERRORS as exc:
6072
pytest.skip(str(exc))
6173

6274

@@ -207,21 +219,45 @@ def test_arch_constraints_hopper_plus(self, init_cuda, sm_resource):
207219

208220

209221
class TestWorkqueueResource:
210-
def test_query(self, wq_resource):
222+
def test_query(self, init_cuda, wq_resource):
211223
assert wq_resource.handle != 0
224+
assert isinstance(wq_resource.sharing_scope, WorkqueueSharingScopeType)
225+
assert wq_resource.concurrency_limit >= 1
226+
assert wq_resource.device.device_id == init_cuda.device_id
212227

213228
def test_configure_none_is_noop(self, wq_resource):
214229
assert wq_resource.configure(WorkqueueResourceOptions(sharing_scope=None)) is None
215230

216231
def test_configure_valid_scope(self, wq_resource):
217232
wq_resource.configure(WorkqueueResourceOptions(sharing_scope="green_ctx_balanced"))
218233

234+
@pytest.mark.parametrize("scope", list(WorkqueueSharingScopeType))
235+
def test_configure_scope_with_enum(self, wq_resource, scope):
236+
wq_resource.configure(WorkqueueResourceOptions(sharing_scope=scope))
237+
assert wq_resource.sharing_scope is scope
238+
239+
def test_device_id_matches_source_multi_gpu(self):
240+
from cuda.core import Device, system
241+
242+
if system.get_num_devices() < 2:
243+
pytest.skip("requires 2+ GPUs")
244+
dev0 = Device(0)
245+
dev1 = Device(1)
246+
try:
247+
wq0 = dev0.resources.workqueue
248+
wq1 = dev1.resources.workqueue
249+
except _RESOURCE_UNAVAILABLE_ERRORS as exc:
250+
pytest.skip(str(exc))
251+
assert wq0.device.device_id == 0
252+
assert wq1.device.device_id == 1
253+
219254
def test_invalid_scope_raises_at_construction(self):
220255
with pytest.raises(ValueError, match="Unknown sharing_scope"):
221256
WorkqueueResourceOptions(sharing_scope="bogus")
222257

223258
def test_configure_concurrency_limit(self, wq_resource):
224259
wq_resource.configure(WorkqueueResourceOptions(concurrency_limit=4))
260+
assert wq_resource.concurrency_limit == 4
225261

226262
def test_configure_concurrency_and_scope(self, wq_resource):
227263
wq_resource.configure(

0 commit comments

Comments
 (0)