Skip to content

Commit 1bb3625

Browse files
committed
raise if call with MemcpySrcAccessOrder.DURING_API_CALL requires fallback
1 parent 528d725 commit 1bb3625

8 files changed

Lines changed: 292 additions & 34 deletions

File tree

‎cuda_core/cuda/core/_memory/_buffer.pyi‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,22 @@ class Buffer:
191191
Not accepted with ``LEGACY_DEFAULT_STREAM`` or a capturing stream
192192
(matches :func:`utils.copy_batch`); use ``PER_THREAD_DEFAULT_STREAM``
193193
or :meth:`graph.GraphNode.memcpy` instead. On cuda.bindings/driver
194-
older than CUDA 13.2, the copy falls back to ``cuMemcpyAsync`` with
195-
``options`` silently ignored.
194+
older than CUDA 13.2, ``src_access_order`` values of ``STREAM``
195+
and ``ANY`` fall back to ``cuMemcpyAsync`` silently; ``DURING_API_CALL``
196+
raises instead of silently downgrading its guarantee.
196197
197198
Raises
198199
------
199200
TypeError
200201
If ``options`` is not a :class:`~utils.CopyOptions` instance, or
201202
if ``options`` is given together with ``LEGACY_DEFAULT_STREAM``
202203
or a stream currently in graph capture mode.
204+
RuntimeError
205+
If ``options.src_access_order`` is ``DURING_API_CALL`` and
206+
cuda.bindings/driver older than CUDA 13.2 makes the native
207+
``cuMemcpyWithAttributesAsync`` path unavailable: the
208+
``cuMemcpyAsync`` fallback reads the source in stream order
209+
only, which cannot honor that guarantee.
203210
204211
"""
205212

@@ -218,15 +225,22 @@ class Buffer:
218225
Not accepted with ``LEGACY_DEFAULT_STREAM`` or a capturing stream
219226
(matches :func:`utils.copy_batch`); use ``PER_THREAD_DEFAULT_STREAM``
220227
or :meth:`graph.GraphNode.memcpy` instead. On cuda.bindings/driver
221-
older than CUDA 13.2, the copy falls back to ``cuMemcpyAsync`` with
222-
``options`` silently ignored.
228+
older than CUDA 13.2, ``src_access_order`` values of ``STREAM``
229+
and ``ANY`` fall back to ``cuMemcpyAsync`` silently; ``DURING_API_CALL``
230+
raises instead of silently downgrading its guarantee.
223231
224232
Raises
225233
------
226234
TypeError
227235
If ``options`` is not a :class:`~utils.CopyOptions` instance, or
228236
if ``options`` is given together with ``LEGACY_DEFAULT_STREAM``
229237
or a stream currently in graph capture mode.
238+
RuntimeError
239+
If ``options.src_access_order`` is ``DURING_API_CALL`` and
240+
cuda.bindings/driver older than CUDA 13.2 makes the native
241+
``cuMemcpyWithAttributesAsync`` path unavailable: the
242+
``cuMemcpyAsync`` fallback reads the source in stream order
243+
only, which cannot honor that guarantee.
230244
"""
231245

232246
def fill(self, value: int | BufferProtocol, *, stream: Stream | GraphBuilder) -> None:

‎cuda_core/cuda/core/_memory/_buffer.pyx‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import sys
4040
from collections.abc import Sequence
4141
from typing import TYPE_CHECKING
4242

43-
from cuda.core._memory._copy_enums import CopyOptions
43+
from cuda.core._memory._copy_enums import CopyOptions, _reject_unsupported_during_api_call
4444
from cuda.core._utils.pycompat import BufferProtocol
4545
from cuda.core._dlpack import classify_dl_device, make_py_capsule
4646
from cuda.core._device import Device
@@ -221,8 +221,14 @@ cdef void _dispatch_buffer_copy(
221221
if _with_attributes_available():
222222
_do_copy_with_attributes(dst, src, nbytes, options, as_cu(s._h_stream))
223223
else:
224-
# Matches copy_batch: options are silently ignored on the
225-
# pre-CUDA-13.2 driver/bindings fallback path.
224+
_reject_unsupported_during_api_call(
225+
options.src_access_order,
226+
"cuda.bindings and the driver to both report CUDA 13.2 or newer "
227+
"(cuMemcpyWithAttributesAsync is unavailable here)",
228+
)
229+
# STREAM and ANY never require access sooner than stream order, so
230+
# cuMemcpyAsync satisfies them; options are otherwise silently
231+
# ignored on this pre-CUDA-13.2 fallback path, matching copy_batch.
226232
with nogil:
227233
HANDLE_RETURN(cydriver.cuMemcpyAsync(dst, src, nbytes, as_cu(s._h_stream)))
228234

@@ -482,15 +488,22 @@ cdef class Buffer:
482488
Not accepted with ``LEGACY_DEFAULT_STREAM`` or a capturing stream
483489
(matches :func:`utils.copy_batch`); use ``PER_THREAD_DEFAULT_STREAM``
484490
or :meth:`graph.GraphNode.memcpy` instead. On cuda.bindings/driver
485-
older than CUDA 13.2, the copy falls back to ``cuMemcpyAsync`` with
486-
``options`` silently ignored.
491+
older than CUDA 13.2, ``src_access_order`` values of ``STREAM``
492+
and ``ANY`` fall back to ``cuMemcpyAsync`` silently; ``DURING_API_CALL``
493+
raises instead of silently downgrading its guarantee.
487494

488495
Raises
489496
------
490497
TypeError
491498
If ``options`` is not a :class:`~utils.CopyOptions` instance, or
492499
if ``options`` is given together with ``LEGACY_DEFAULT_STREAM``
493500
or a stream currently in graph capture mode.
501+
RuntimeError
502+
If ``options.src_access_order`` is ``DURING_API_CALL`` and
503+
cuda.bindings/driver older than CUDA 13.2 makes the native
504+
``cuMemcpyWithAttributesAsync`` path unavailable: the
505+
``cuMemcpyAsync`` fallback reads the source in stream order
506+
only, which cannot honor that guarantee.
494507

495508
"""
496509
cdef Stream s = Stream_accept(stream)
@@ -527,15 +540,22 @@ cdef class Buffer:
527540
Not accepted with ``LEGACY_DEFAULT_STREAM`` or a capturing stream
528541
(matches :func:`utils.copy_batch`); use ``PER_THREAD_DEFAULT_STREAM``
529542
or :meth:`graph.GraphNode.memcpy` instead. On cuda.bindings/driver
530-
older than CUDA 13.2, the copy falls back to ``cuMemcpyAsync`` with
531-
``options`` silently ignored.
543+
older than CUDA 13.2, ``src_access_order`` values of ``STREAM``
544+
and ``ANY`` fall back to ``cuMemcpyAsync`` silently; ``DURING_API_CALL``
545+
raises instead of silently downgrading its guarantee.
532546

533547
Raises
534548
------
535549
TypeError
536550
If ``options`` is not a :class:`~utils.CopyOptions` instance, or
537551
if ``options`` is given together with ``LEGACY_DEFAULT_STREAM``
538552
or a stream currently in graph capture mode.
553+
RuntimeError
554+
If ``options.src_access_order`` is ``DURING_API_CALL`` and
555+
cuda.bindings/driver older than CUDA 13.2 makes the native
556+
``cuMemcpyWithAttributesAsync`` path unavailable: the
557+
``cuMemcpyAsync`` fallback reads the source in stream order
558+
only, which cannot honor that guarantee.
539559
"""
540560
cdef Stream s = Stream_accept(stream)
541561
cdef size_t dst_size = self._size

‎cuda_core/cuda/core/_memory/_copy_enums.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,40 @@ def _to_driver_flags(self) -> int:
160160
_OVERLAP_MODE_TO_DRIVER = {}
161161

162162

163+
def _reject_unsupported_during_api_call(
164+
src_access_order: MemcpySrcAccessOrder, requirement: str, *, index: int | None = None
165+
) -> None:
166+
"""Raise if ``src_access_order`` is DURING_API_CALL but the native attributes
167+
path (``cuMemcpyWithAttributesAsync`` / ``cuMemcpyBatchAsync``) is unavailable.
168+
169+
STREAM and ANY never promise access sooner than stream order, so a plain
170+
``cuMemcpyAsync`` fallback satisfies them; DURING_API_CALL specifically
171+
promises all source reads complete before the call returns, which
172+
``cuMemcpyAsync`` cannot provide (it reads the source in stream order
173+
only). Silently downgrading that guarantee would let a caller reuse or
174+
overwrite the source buffer before the real, stream-ordered read
175+
happens: a silent data race, not a missed optimization. ``requirement``
176+
names what the native path needs and why it is unavailable here;
177+
``index`` identifies the offending copy within a batch.
178+
179+
Internal, but deliberately importable: shared between the per-buffer and
180+
batched fallback paths so both raise identically, and directly testable
181+
without needing an actual old driver/bindings install.
182+
"""
183+
if src_access_order != MemcpySrcAccessOrder.DURING_API_CALL:
184+
return
185+
where = f" at index {index}" if index is not None else ""
186+
raise RuntimeError(
187+
f"src_access_order=DURING_API_CALL{where} requires {requirement}. A "
188+
"plain cuMemcpyAsync fallback reads the source in stream order only, "
189+
"which would silently violate the guarantee that all source reads "
190+
"complete before the call returns, letting the caller reuse the "
191+
"source buffer before the real (stream-ordered) read happens. Use "
192+
"src_access_order=STREAM or ANY, or omit options, if that works for "
193+
"your use case."
194+
)
195+
196+
163197
def _attr_run_starts(attrs: Sequence[CopyOptions]) -> list[int]:
164198
"""Return the start index of each maximal run of equal attributes.
165199

‎cuda_core/cuda/core/_memory/_copy_ops.pyi‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ def copy_batch(stream: Stream, srcs: Sequence[Buffer], dsts: Sequence[Buffer], *
6363
If a single Buffer is passed instead of a sequence, if
6464
``LEGACY_DEFAULT_STREAM`` is passed, or if the stream is currently
6565
in graph capture mode.
66+
RuntimeError
67+
If any copy requests ``src_access_order=DURING_API_CALL`` and the
68+
native ``cuMemcpyBatchAsync`` path is unavailable (see Notes): the
69+
per-copy ``cuMemcpyAsync`` fallback reads the source in stream
70+
order only, which cannot honor that guarantee.
6671
6772
Notes
6873
-----
@@ -81,7 +86,11 @@ def copy_batch(stream: Stream, srcs: Sequence[Buffer], dsts: Sequence[Buffer], *
8186
8287
On pre-CUDA 13 installs the copies fall back to a Python-level loop
8388
over ``cuMemcpyAsync``, so the potential performance benefit of
84-
asynchronous batched copies is not realized. :class:`CopyOptions` are
85-
silently ignored on the fallback path.
89+
asynchronous batched copies is not realized. ``src_access_order`` values
90+
of ``STREAM`` and ``ANY`` are silently ignored on the fallback path
91+
(stream-ordered access already satisfies both); ``DURING_API_CALL``
92+
raises ``RuntimeError`` instead, since silently downgrading it to
93+
stream-ordered access would let a caller reuse the source buffer before
94+
the real read happens.
8695
8796
"""

‎cuda_core/cuda/core/_memory/_copy_ops.pyx‎

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ from cuda.core._utils.cuda_utils cimport HANDLE_RETURN
2121
# a pragma to be seen as used.
2222
from cuda.core._utils.version cimport cy_driver_version # no-cython-lint
2323

24-
from cuda.core._memory._copy_enums import CopyOptions, _attr_run_starts # no-cython-lint
24+
from cuda.core._memory._copy_enums import (
25+
CopyOptions,
26+
_attr_run_starts, # no-cython-lint
27+
_reject_unsupported_during_api_call,
28+
)
2529

2630
_SINGLE_COPY_HINT = "Buffer.copy_to / Buffer.copy_from"
2731

@@ -131,6 +135,11 @@ def copy_batch(
131135
If a single Buffer is passed instead of a sequence, if
132136
``LEGACY_DEFAULT_STREAM`` is passed, or if the stream is currently
133137
in graph capture mode.
138+
RuntimeError
139+
If any copy requests ``src_access_order=DURING_API_CALL`` and the
140+
native ``cuMemcpyBatchAsync`` path is unavailable (see Notes): the
141+
per-copy ``cuMemcpyAsync`` fallback reads the source in stream
142+
order only, which cannot honor that guarantee.
134143

135144
Notes
136145
-----
@@ -149,8 +158,12 @@ def copy_batch(
149158

150159
On pre-CUDA 13 installs the copies fall back to a Python-level loop
151160
over ``cuMemcpyAsync``, so the potential performance benefit of
152-
asynchronous batched copies is not realized. :class:`CopyOptions` are
153-
silently ignored on the fallback path.
161+
asynchronous batched copies is not realized. ``src_access_order`` values
162+
of ``STREAM`` and ``ANY`` are silently ignored on the fallback path
163+
(stream-ordered access already satisfies both); ``DURING_API_CALL``
164+
raises ``RuntimeError`` instead, since silently downgrading it to
165+
stream-ordered access would let a caller reuse the source buffer before
166+
the real read happens.
154167

155168
"""
156169
cdef tuple src_bufs = Buffer_coerce_batch(srcs, "copy_batch", _SINGLE_COPY_HINT)
@@ -211,17 +224,36 @@ cdef void _do_copy_batch(tuple src_bufs, tuple dst_bufs, Stream s, tuple attr_tu
211224
if _batch_entry_point_available():
212225
_do_copy_batch_native(src_bufs, dst_bufs, s, attr_tuple)
213226
else:
227+
_reject_during_api_call_fallback(attr_tuple)
214228
_do_copy_batch_loop(src_bufs, dst_bufs, s)
215229
ELSE:
230+
_reject_during_api_call_fallback(attr_tuple)
216231
_do_copy_batch_loop(src_bufs, dst_bufs, s)
217232
218233
234+
cdef void _reject_during_api_call_fallback(tuple attr_tuple):
235+
"""Raise before the per-copy cuMemcpyAsync loop if any copy needs
236+
DURING_API_CALL, which that fallback cannot honor (see
237+
_reject_unsupported_during_api_call for why this must raise rather than
238+
silently ignore the option, unlike STREAM and ANY).
239+
"""
240+
cdef Py_ssize_t i
241+
for i in range(len(attr_tuple)):
242+
_reject_unsupported_during_api_call(
243+
(<object>attr_tuple[i]).src_access_order,
244+
"cuda.core built against CUDA 13 headers and cuda.bindings/driver "
245+
"13.0 or newer (cuMemcpyBatchAsync is unavailable here)",
246+
index=i,
247+
)
248+
249+
219250
cdef void _do_copy_batch_loop(tuple src_bufs, tuple dst_bufs, Stream s):
220251
"""Per-copy cuMemcpyAsync fallback where the batch entry point is absent.
221252

222253
Issues copies one at a time, so the performance benefit of batching is
223-
not realized. Callers guarantee the options are defaults; copy_batch
224-
rejects anything else before reaching here.
254+
not realized. STREAM and ANY are silently ignored here (satisfied by
255+
stream-ordered cuMemcpyAsync regardless); DURING_API_CALL is rejected by
256+
_reject_during_api_call_fallback before this is ever called.
225257
"""
226258
cdef Py_ssize_t n = len(src_bufs)
227259
cdef Py_ssize_t i

‎cuda_core/docs/source/release/1.2.0-notes.rst‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ New features
1414
by the new :class:`utils.CopyOptions` dataclass. Requires ``cuda.core``
1515
built against CUDA 13, ``cuda.bindings`` 13.0+, and a driver reporting
1616
CUDA 13.0 or newer; otherwise falls back to a per-copy ``cuMemcpyAsync``
17-
loop with options silently ignored. Graph capture and
17+
loop. On that fallback, ``src_access_order`` values of ``STREAM`` and
18+
``ANY`` are silently ignored (stream-ordered access already satisfies
19+
both), while ``DURING_API_CALL`` raises ``RuntimeError`` instead of
20+
silently downgrading its stronger guarantee. Graph capture and
1821
``LEGACY_DEFAULT_STREAM`` are rejected (``PER_THREAD_DEFAULT_STREAM`` is
1922
accepted). Copies within a batch must not alias.
2023
(`#1333 <https://github.com/NVIDIA/cuda-python/issues/1333>`__)
@@ -32,8 +35,10 @@ New features
3235
``cuMemcpyWithAttributesAsync``. Matching :func:`utils.copy_batch`, passing
3336
``options`` together with ``LEGACY_DEFAULT_STREAM`` or a capturing stream
3437
raises ``TypeError`` (``PER_THREAD_DEFAULT_STREAM`` is accepted). On
35-
``cuda.bindings``/driver older than CUDA 13.2, the copy falls back to
36-
``cuMemcpyAsync`` with ``options`` silently ignored.
38+
``cuda.bindings``/driver older than CUDA 13.2, ``src_access_order`` values
39+
of ``STREAM`` and ``ANY`` fall back to ``cuMemcpyAsync`` silently, while
40+
``DURING_API_CALL`` raises ``RuntimeError`` instead of silently
41+
downgrading its stronger guarantee.
3742
(`#2365 <https://github.com/NVIDIA/cuda-python/issues/2365>`__)
3843

3944
Fixes and enhancements

0 commit comments

Comments
 (0)