Skip to content

Commit fd58e8f

Browse files
committed
fallback for CUDA 12 and type annotations
1 parent bf41f93 commit fd58e8f

12 files changed

Lines changed: 341 additions & 156 deletions

File tree

cuda_core/cuda/core/_memory/_buffer.pxd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,9 @@ cdef Buffer Buffer_from_deviceptr_handle(
4444
object ipc_descriptor = *,
4545
type cls = *,
4646
)
47+
48+
49+
# Shared argument coercion for the batched free functions (copy_batch,
50+
# prefetch_batch, discard_batch, discard_prefetch_batch). `single_hint`
51+
# names the per-buffer API to use instead when a bare Buffer is passed.
52+
cdef tuple Buffer_coerce_batch(object buffers, str what, str single_hint)

cuda_core/cuda/core/_memory/_buffer.pyx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ from cuda.core._stream cimport Stream, Stream_accept, default_stream
2929
from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value
3030

3131
import sys
32+
from collections.abc import Sequence
3233
from typing import TYPE_CHECKING
3334

3435
from cuda.core._utils.pycompat import BufferProtocol
@@ -619,6 +620,32 @@ cdef Buffer Buffer_from_deviceptr_handle(
619620
return buf
620621

621622

623+
cdef tuple Buffer_coerce_batch(object buffers, str what, str single_hint):
624+
"""Coerce ``buffers`` to a ``tuple[Buffer, ...]``; reject a bare Buffer.
625+
626+
Shared by the batched free functions. Passing one Buffer is rejected
627+
rather than treated as a one-element batch so that the per-buffer API
628+
named by ``single_hint`` stays the single obvious way to do it.
629+
"""
630+
cdef list out
631+
if isinstance(buffers, Buffer):
632+
raise TypeError(
633+
f"{what}: pass a sequence of Buffers; for a single buffer use {single_hint}"
634+
)
635+
if not isinstance(buffers, Sequence):
636+
raise TypeError(
637+
f"{what}: buffers must be a sequence of Buffer, got {type(buffers).__name__}"
638+
)
639+
if not buffers:
640+
raise ValueError(f"{what}: empty buffers sequence")
641+
out = []
642+
for item in buffers:
643+
if not isinstance(item, Buffer):
644+
raise TypeError(f"{what}: expected Buffer, got {type(item).__name__}")
645+
out.append(item)
646+
return tuple(out)
647+
648+
622649
cdef inline void Buffer_close(Buffer self, object stream):
623650
"""Close a buffer, freeing its memory."""
624651
cdef Stream s

cuda_core/cuda/core/_memory/_copy_enums.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
from __future__ import annotations
66

77
import dataclasses
8-
import functools
98
from collections.abc import Sequence
109
from typing import TYPE_CHECKING
1110

1211
from cuda.core._utils.cuda_utils import driver
1312
from cuda.core._utils.pycompat import StrEnum
13+
from cuda.core._utils.version import binding_version
1414

1515
if TYPE_CHECKING:
1616
from cuda.core._device import Device
@@ -94,39 +94,46 @@ def __post_init__(self):
9494

9595
def _to_driver_enum(self) -> int:
9696
"""Return the driver CUmemcpySrcAccessOrder value."""
97-
return _src_access_order_to_cu()[MemcpySrcAccessOrder(self.src_access_order)]
97+
if not _SRC_ACCESS_ORDER_TO_DRIVER:
98+
raise NotImplementedError(_CUDA13_REQUIRED)
99+
return _SRC_ACCESS_ORDER_TO_DRIVER[MemcpySrcAccessOrder(self.src_access_order)]
98100

99101
def _to_driver_flags(self) -> int:
100102
"""Return the driver CUmemcpyFlags value."""
101-
return _overlap_mode_to_cu()[MemcpyOverlapMode(self.overlap_mode)]
103+
if not _OVERLAP_MODE_TO_DRIVER:
104+
raise NotImplementedError(_CUDA13_REQUIRED)
105+
return _OVERLAP_MODE_TO_DRIVER[MemcpyOverlapMode(self.overlap_mode)]
102106

103107

104-
# Bridges between the public StrEnums and the driver integer values. Built on
105-
# first use rather than at import: the CUmemcpy* enums only exist on toolkits
106-
# that ship the batched memcpy entry points, and importing cuda.core must not
107-
# depend on them.
108+
_CUDA13_REQUIRED = "copy attributes require a CUDA 13 build of cuda-bindings"
109+
110+
# CUmemcpySrcAccessOrder and CUmemcpyFlags are CUDA 13 additions, so these
111+
# maps are empty on a CUDA 12 build. Nothing reaches them there: copy_batch
112+
# refuses non-default CopyOptions when the batched entry point is absent.
108113
#
109-
# Keyed by ``str`` rather than by the enum: under ``python_version = "3.10"``
110-
# mypy resolves ``StrEnum`` to the unstubbed ``backports.strenum`` shim and so
111-
# infers the members as plain ``str``. StrEnum members are ``str`` instances,
112-
# so this annotation is accurate on every supported version.
113-
@functools.cache
114-
def _src_access_order_to_cu() -> dict[str, int]:
115-
cu = driver.CUmemcpySrcAccessOrder
116-
return {
117-
MemcpySrcAccessOrder.STREAM: int(cu.CU_MEMCPY_SRC_ACCESS_ORDER_STREAM),
118-
MemcpySrcAccessOrder.DURING_API_CALL: int(cu.CU_MEMCPY_SRC_ACCESS_ORDER_DURING_API_CALL),
119-
MemcpySrcAccessOrder.ANY: int(cu.CU_MEMCPY_SRC_ACCESS_ORDER_ANY),
114+
# Keyed by ``str``: under ``python_version = "3.10"`` mypy resolves StrEnum to
115+
# the unstubbed backports shim and so infers the members as plain ``str``.
116+
# StrEnum members are ``str`` instances, so this holds on every version. The
117+
# values are wrapped in ``int()`` because the driver enums are untyped.
118+
_SRC_ACCESS_ORDER_TO_DRIVER: dict[str, int]
119+
_OVERLAP_MODE_TO_DRIVER: dict[str, int]
120+
121+
if binding_version() >= (13, 0, 0):
122+
_src_order = driver.CUmemcpySrcAccessOrder
123+
_flags = driver.CUmemcpyFlags
124+
_SRC_ACCESS_ORDER_TO_DRIVER = {
125+
MemcpySrcAccessOrder.STREAM: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_STREAM),
126+
MemcpySrcAccessOrder.DURING_API_CALL: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_DURING_API_CALL),
127+
MemcpySrcAccessOrder.ANY: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_ANY),
120128
}
121-
122-
123-
@functools.cache
124-
def _overlap_mode_to_cu() -> dict[str, int]:
125-
cu = driver.CUmemcpyFlags
126-
return {
127-
MemcpyOverlapMode.DEFAULT: int(cu.CU_MEMCPY_FLAG_DEFAULT),
128-
MemcpyOverlapMode.PREFER_OVERLAP_WITH_COMPUTE: int(cu.CU_MEMCPY_FLAG_PREFER_OVERLAP_WITH_COMPUTE),
129+
_OVERLAP_MODE_TO_DRIVER = {
130+
MemcpyOverlapMode.DEFAULT: int(_flags.CU_MEMCPY_FLAG_DEFAULT),
131+
MemcpyOverlapMode.PREFER_OVERLAP_WITH_COMPUTE: int(_flags.CU_MEMCPY_FLAG_PREFER_OVERLAP_WITH_COMPUTE),
129132
}
133+
del _src_order, _flags
134+
else:
135+
_SRC_ACCESS_ORDER_TO_DRIVER = {}
136+
_OVERLAP_MODE_TO_DRIVER = {}
130137

131138

132139
def _attr_run_starts(attrs: Sequence[CopyOptions]) -> list[int]:

cuda_core/cuda/core/_memory/_copy_ops.pyi

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@ from __future__ import annotations
55
from collections.abc import Sequence
66

77
from cuda.core._memory._buffer import Buffer
8+
from cuda.core._memory._copy_enums import CopyOptions
9+
from cuda.core._stream import Stream
810

11+
_SINGLE_COPY_HINT = 'Buffer.copy_to / Buffer.copy_from'
12+
_DEFAULT_COPY_OPTIONS = CopyOptions()
913

10-
def copy_batch(stream: object, srcs: Sequence[Buffer], dsts: Sequence[Buffer], *, options: object=None) -> None:
14+
def _batch_entry_point_in_use() -> bool:
15+
"""Internal: expose the dispatch predicate so tests can gate on it."""
16+
17+
def copy_batch(stream: Stream, srcs: Sequence[Buffer], dsts: Sequence[Buffer], *, options: CopyOptions | Sequence[CopyOptions] | None=None) -> None:
1118
"""Copy a batch of buffers asynchronously.
1219
13-
Requires CUDA 13+. For a single buffer, use
14-
:meth:`Buffer.copy_to` or :meth:`Buffer.copy_from`.
20+
Sizes are taken from the source buffers and each destination must
21+
match. For a single buffer, use :meth:`Buffer.copy_to` or
22+
:meth:`Buffer.copy_from`.
1523
1624
The driver provides no graph-node form of ``cuMemcpyBatchAsync``, so
1725
this cannot be captured into a graph. Build graph copies with
@@ -20,26 +28,41 @@ def copy_batch(stream: object, srcs: Sequence[Buffer], dsts: Sequence[Buffer], *
2028
Parameters
2129
----------
2230
stream : :class:`~_stream.Stream`
23-
Stream for the asynchronous copy. Passing a
24-
:class:`~graph.GraphBuilder` raises ``CUDAError``
25-
(``CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED``).
31+
Stream for the asynchronous copy. First positional and required
32+
(mirrors :func:`launch`). Unlike most stream-taking APIs this does
33+
not accept a :class:`~graph.GraphBuilder`; one is rejected with
34+
``TypeError`` because the copy cannot be captured.
2635
srcs : Sequence[:class:`Buffer`]
27-
Source buffers. Must be a sequence, not a single Buffer.
36+
Source buffers. Must be a sequence, not a single Buffer.
2837
dsts : Sequence[:class:`Buffer`]
29-
Destination buffers. Must match ``len(srcs)``.
38+
Destination buffers. Must match ``len(srcs)``.
3039
options : :class:`CopyOptions` | Sequence[:class:`CopyOptions`] | None
3140
Per-copy options. A single value applies to every copy; a
3241
sequence pairs by index and must match ``len(srcs)``. ``None``
3342
uses stream-ordered defaults.
3443
3544
Raises
3645
------
37-
NotImplementedError
38-
On a CUDA 12 build of ``cuda.core``.
3946
ValueError
4047
If lengths or sizes mismatch.
4148
TypeError
4249
If a single Buffer is passed instead of a sequence.
50+
NotImplementedError
51+
If non-default ``options`` are given where
52+
``cuMemcpyBatchAsync`` is unavailable (see Notes).
53+
54+
Notes
55+
-----
56+
``cuMemcpyBatchAsync`` needs both a CUDA 13 build of ``cuda.core``
57+
and a CUDA 13 driver. Otherwise the copies fall back to a
58+
Python-level loop over ``cuMemcpyAsync``, which is semantically
59+
equivalent but does not amortize launch overhead. That fallback has
60+
no way to convey :class:`CopyOptions` to the driver, so non-default
61+
options raise :class:`NotImplementedError` there rather than being
62+
silently ignored.
63+
64+
Warns
65+
-----
4366
UserWarning
4467
If ``overlap_mode='prefer_overlap_with_compute'`` is requested
4568
on a non-integrated (discrete) GPU.

0 commit comments

Comments
 (0)