Coverage for cuda/core/_memory/_copy_ops.pyx: 70.45%
88 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 01:23 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 01:23 +0000
1# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2#
3# SPDX-License-Identifier: Apache-2.0
5from __future__ import annotations
7from collections.abc import Sequence
9IF CUDA_CORE_BUILD_MAJOR >= 13:
10 from libcpp.vector cimport vector
12from cuda.bindings cimport cydriver
13from cuda.core._memory._buffer cimport Buffer, Buffer_coerce_batch
14from cuda.core._memory._copy_attributes cimport _to_cu_memcpy_attributes # no-cython-lint
15from cuda.core._rt cimport as_cu
16from cuda.core._stream cimport Stream, Stream_accept, Stream_is_legacy_default_token
17from cuda.core._utils.cuda_utils cimport HANDLE_RETURN
19# cy_driver_version and _attr_run_starts are referenced only from CUDA 13
20# branches. cython-lint does not evaluate compile-time IF blocks, so they need
21# a pragma to be seen as used.
22from cuda.core._utils.version cimport cy_driver_version # no-cython-lint
24from cuda.core._memory._copy_enums import (
25 CopyOptions,
26 _attr_run_starts, # no-cython-lint
27 _reject_unsupported_during_api_call,
28)
30_SINGLE_COPY_HINT = "Buffer.copy_to / Buffer.copy_from"
33cdef inline bint _batch_entry_point_available():
34 """Whether cuMemcpyBatchAsync can actually be called here.
36 Requires ``cuda.core`` built against CUDA 13 headers (compile time) and
37 a driver reporting CUDA 13.0 or newer, i.e.
38 ``cuDriverGetVersion() >= 13000`` (run time).
40 The run-time bound is set by the binding layer, not by when the driver
41 gained the feature. CUDA 12.8 already exposed a ``cuMemcpyBatchAsync``,
42 but its signature carried a ``failIdx`` out-parameter that CUDA 13.0
43 dropped. ``cuda.bindings`` resolves only the 13.0 revision, via
44 ``cuGetProcAddress_v2('cuMemcpyBatchAsync', ..., 13000, ...)``, so an
45 older driver yields a NULL pointer even though it may implement the
46 earlier entry point.
47 """
48 IF CUDA_CORE_BUILD_MAJOR >= 13:
49 return cy_driver_version() >= (13, 0, 0) 1klmnopqrstbcdefghaij
50 ELSE:
51 return False
54def _normalize_copy_options(
55 options: CopyOptions | Sequence[CopyOptions] | None,
56 Py_ssize_t n,
57) -> tuple[CopyOptions, ...]:
58 """Expand ``options`` to exactly one :class:`CopyOptions` per copy.
60 ``None`` and a scalar broadcast; a sequence pairs by index and must
61 already have length ``n``.
63 Internal, but deliberately importable: options are hints that change
64 how the driver stages a transfer and never the bytes it produces, so
65 this expansion (and the run encoding applied to it) is the only
66 observable evidence that a scalar reached every copy.
67 """
68 if options is None: 1klmnopqrstbcdefghaijvuwEAB
69 return (CopyOptions(),) * n 1klmnopqrstE
70 if isinstance(options, CopyOptions): 1bcdefghaijvuwAB
71 return (options,) * n 1bcdefghijA
72 if isinstance(options, Sequence): 1avuwAB
73 if len(options) != n: 1avuAB
74 raise ValueError( 1vB
75 f"copy_batch: options length {len(options)} does not match " 1vB
76 f"buffers length {n}" 1vB
77 )
78 for a in options: 1auA
79 if not isinstance(a, CopyOptions): 1auA
80 raise TypeError( 1u
81 f"copy_batch: each options element must be CopyOptions, " 1u
82 f"got {type(a).__name__}" 1u
83 )
84 return tuple(options) 1aA
85 raise TypeError( 1w
86 f"copy_batch: options must be CopyOptions or a sequence of " 1w
87 f"CopyOptions, got {type(options).__name__}" 1w
88 )
91def copy_batch(
92 Stream stream not None: Stream,
93 srcs: Sequence[Buffer],
94 dsts: Sequence[Buffer],
95 *,
96 options: CopyOptions | Sequence[CopyOptions] | None = None,
97) -> None:
98 """Copy a batch of buffers asynchronously.
100 Source buffer and destination buffer sizes must match. For a single
101 buffer, use :meth:`Buffer.copy_to` or :meth:`Buffer.copy_from`.
103 The driver provides no graph-node form of ``cuMemcpyBatchAsync``, so
104 this cannot be captured into a graph. Both passing a
105 :class:`~graph.GraphBuilder` and passing its underlying
106 :attr:`~graph.GraphBuilder.stream` while capture is active are
107 rejected. Build graph copies with
108 :meth:`graph.GraphNode.memcpy` or per-buffer :meth:`Buffer.copy_to`.
110 Parameters
111 ----------
112 stream : :class:`~_stream.Stream`
113 Stream for the asynchronous copy. First positional and required
114 (mirrors :func:`launch`). Does not accept a capturing stream
115 (including a :class:`~graph.GraphBuilder`\'s underlying stream); use
116 :meth:`graph.GraphNode.memcpy` or per-buffer
117 :meth:`Buffer.copy_to` to build copies into a graph. Does not accept
118 ``LEGACY_DEFAULT_STREAM``, which ``cuMemcpyBatchAsync`` rejects
119 outright; ``PER_THREAD_DEFAULT_STREAM`` is a real stream to the
120 driver and is accepted.
121 srcs : Sequence[:class:`Buffer`]
122 Source buffers. Must be a sequence, not a single Buffer.
123 dsts : Sequence[:class:`Buffer`]
124 Destination buffers. Must match ``len(srcs)``.
125 options : :class:`CopyOptions` | Sequence[:class:`CopyOptions`] | None
126 Per-copy options. A single value applies to every copy; a
127 sequence pairs by index and must match ``len(srcs)``. ``None``
128 uses stream-ordered defaults.
130 Raises
131 ------
132 ValueError
133 If lengths or sizes mismatch.
134 TypeError
135 If a single Buffer is passed instead of a sequence, if
136 ``LEGACY_DEFAULT_STREAM`` is passed, or if the stream is currently
137 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.
144 Notes
145 -----
146 Batching through ``cuMemcpyBatchAsync`` requires both the CUDA 13 build of
147 ``cuda.core`` and a driver that reports CUDA 13.0 or newer
148 (``cuDriverGetVersion() >= 13000``). ``cuda.bindings`` binds only the
149 CUDA 13.0 revision of the entry point, so a driver that predates it is
150 refused even where it implements the earlier CUDA 12.8 signature.
152 The driver may execute batch items concurrently and in any order.
153 A batch must therefore not contain copies where the source range of
154 one copy overlaps the destination range of another; such aliasing
155 produces undefined results. Detecting overlaps at runtime is
156 impractical; callers are responsible for ensuring no aliasing exists.
158 On pre-CUDA 13 installs the copies fall back to a Python-level loop
159 over ``cuMemcpyAsync``, so the potential performance benefit of
160 asynchronous batched copies is not realized. ``src_access_order`` values
161 of ``STREAM`` and ``ANY`` are silently ignored on the fallback path
162 (stream-ordered access already satisfies both); ``DURING_API_CALL``
163 raises ``RuntimeError`` instead, since silently downgrading it to
164 stream-ordered access would let a caller reuse the source buffer before
165 the real read happens.
167 """
168 cdef tuple src_bufs = Buffer_coerce_batch(srcs, "copy_batch", _SINGLE_COPY_HINT) 1klmnopqrzCstbcdefghaijDvuwFGHIxy
169 cdef tuple dst_bufs = Buffer_coerce_batch(dsts, "copy_batch", _SINGLE_COPY_HINT) 1klmnopqrzCstbcdefghaijDvuwFGHIxy
170 cdef Py_ssize_t n = len(src_bufs) 1klmnopqrzCstbcdefghaijDvuwxy
172 if len(dst_bufs) != n: 1klmnopqrzCstbcdefghaijDvuwxy
173 raise ValueError( 1D
174 f"copy_batch: srcs length {n} does not match dsts length {len(dst_bufs)}" 1D
175 )
177 cdef Stream s = Stream_accept(stream) 1klmnopqrzCstbcdefghaijvuwxy
179 if Stream_is_legacy_default_token(s): 1klmnopqrzCstbcdefghaijvuwxy
180 raise TypeError( 1C
181 "copy_batch does not accept LEGACY_DEFAULT_STREAM; cuMemcpyBatchAsync "
182 "rejects it outright, unlike PER_THREAD_DEFAULT_STREAM, which is a real "
183 "stream to the driver and is accepted. Pass an explicit stream or "
184 "PER_THREAD_DEFAULT_STREAM."
185 )
187 cdef cydriver.CUstreamCaptureStatus _cap_status
188 IF CUDA_CORE_BUILD_MAJOR >= 13:
189 HANDLE_RETURN(cydriver.cuStreamGetCaptureInfo(as_cu(s._h_stream), &_cap_status, 1klmnopqrzstbcdefghaijvuwxy
190 NULL, NULL, NULL, NULL, NULL))
191 ELSE:
192 HANDLE_RETURN(cydriver.cuStreamGetCaptureInfo(as_cu(s._h_stream), &_cap_status,
193 NULL, NULL, NULL, NULL))
194 if _cap_status == cydriver.CU_STREAM_CAPTURE_STATUS_ACTIVE: 1klmnopqrzstbcdefghaijvuwxy
195 raise TypeError( 1z
196 "copy_batch does not support graph capture; "
197 "use GraphNode.memcpy or per-buffer Buffer.copy_to instead"
198 )
200 cdef Buffer src_buf
201 cdef Buffer dst_buf
202 cdef Py_ssize_t i
204 for i in range(n): 1klmnopqrstbcdefghaijvuwxy
205 src_buf = <Buffer>src_bufs[i] 1klmnopqrstbcdefghaijvuwxy
206 dst_buf = <Buffer>dst_bufs[i] 1klmnopqrstbcdefghaijvuwxy
207 if src_buf.size != dst_buf.size: 1klmnopqrstbcdefghaijvuwxy
208 raise ValueError( 1xy
209 f"copy_batch: buffer size mismatch at index {i} " 1xy
210 f"(src={src_buf.size}, dst={dst_buf.size})" 1xy
211 )
213 cdef tuple attr_tuple = _normalize_copy_options(options, n) 1klmnopqrstbcdefghaijvuw
215 _do_copy_batch(src_bufs, dst_bufs, s, attr_tuple) 1klmnopqrstbcdefghaij
218cdef void _do_copy_batch(tuple src_bufs, tuple dst_bufs, Stream s, tuple attr_tuple):
219 IF CUDA_CORE_BUILD_MAJOR >= 13:
220 # Building against CUDA 13 headers says nothing about the installed
221 # driver, so the run-time version still has to be checked before
222 # calling a 13.0-only entry point (see PRs #2054 / #2064).
223 if _batch_entry_point_available(): 1klmnopqrstbcdefghaij
224 _do_copy_batch_native(src_bufs, dst_bufs, s, attr_tuple) 1klmnopqrstbcdefghaij
225 else:
226 _reject_during_api_call_fallback(attr_tuple)
227 _do_copy_batch_loop(src_bufs, dst_bufs, s)
228 ELSE:
229 _reject_during_api_call_fallback(attr_tuple)
230 _do_copy_batch_loop(src_bufs, dst_bufs, s)
233cdef void _reject_during_api_call_fallback(tuple attr_tuple):
234 """Raise before the per-copy cuMemcpyAsync loop if any copy needs
235 DURING_API_CALL, which that fallback cannot honor (see
236 _reject_unsupported_during_api_call for why this must raise rather than
237 silently ignore the option, unlike STREAM and ANY).
238 """
239 cdef Py_ssize_t i
240 for i in range(len(attr_tuple)):
241 _reject_unsupported_during_api_call(
242 (<object>attr_tuple[i]).src_access_order,
243 "the CUDA 13 build of cuda.core and a driver that reports CUDA 13.0 or newer "
244 "(cuMemcpyBatchAsync is unavailable here)",
245 index=i,
246 )
249cdef void _do_copy_batch_loop(tuple src_bufs, tuple dst_bufs, Stream s):
250 """Per-copy cuMemcpyAsync fallback where the batch entry point is absent.
252 Issues copies one at a time, so the performance benefit of batching is
253 not realized. STREAM and ANY are silently ignored here (satisfied by
254 stream-ordered cuMemcpyAsync regardless); DURING_API_CALL is rejected by
255 _reject_during_api_call_fallback before this is ever called.
256 """
257 cdef Py_ssize_t n = len(src_bufs)
258 cdef Py_ssize_t i
259 cdef Buffer src_buf
260 cdef Buffer dst_buf
261 cdef size_t nbytes
262 cdef cydriver.CUstream hstream = as_cu(s._h_stream)
264 for i in range(n):
265 src_buf = <Buffer>src_bufs[i]
266 dst_buf = <Buffer>dst_bufs[i]
267 nbytes = src_buf._size
268 with nogil:
269 HANDLE_RETURN(cydriver.cuMemcpyAsync(
270 as_cu(dst_buf._h_ptr), as_cu(src_buf._h_ptr), nbytes, hstream))
273IF CUDA_CORE_BUILD_MAJOR >= 13:
274 cdef void _do_copy_batch_native(tuple src_bufs, tuple dst_bufs, Stream s, tuple attr_tuple):
275 cdef Py_ssize_t n = len(src_bufs) 1klmnopqrstbcdefghaij
276 cdef cydriver.CUstream hstream = as_cu(s._h_stream) 1klmnopqrstbcdefghaij
277 cdef vector[cydriver.CUdeviceptr] dst_ptrs
278 cdef vector[cydriver.CUdeviceptr] src_ptrs
279 cdef vector[size_t] sizes
280 cdef vector[size_t] attrs_idxs
281 dst_ptrs.resize(n) 1klmnopqrstbcdefghaij
282 src_ptrs.resize(n) 1klmnopqrstbcdefghaij
283 sizes.resize(n) 1klmnopqrstbcdefghaij
285 cdef Buffer src_buf
286 cdef Buffer dst_buf
287 cdef Py_ssize_t i
289 # Collapse equal neighbouring attributes into runs so a broadcast
290 # attribute reaches the driver once (numAttrs == 1) instead of being
291 # repeated per copy. attrs[k] applies to [attrsIdxs[k], attrsIdxs[k+1]).
292 cdef list run_starts = _attr_run_starts(attr_tuple) 1klmnopqrstbcdefghaij
293 cdef vector[cydriver.CUmemcpyAttributes] cu_attrs
294 cdef size_t num_attrs = <size_t>len(run_starts) 1klmnopqrstbcdefghaij
295 cu_attrs.reserve(num_attrs) 1klmnopqrstbcdefghaij
296 attrs_idxs.reserve(num_attrs) 1klmnopqrstbcdefghaij
297 for i in run_starts: 1klmnopqrstbcdefghaij
298 cu_attrs.push_back(_to_cu_memcpy_attributes(attr_tuple[i])) 1klmnopqrstbcdefghaij
299 attrs_idxs.push_back(<size_t>i) 1klmnopqrstbcdefghaij
301 for i in range(n): 1klmnopqrstbcdefghaij
302 src_buf = <Buffer>src_bufs[i] 1klmnopqrstbcdefghaij
303 dst_buf = <Buffer>dst_bufs[i] 1klmnopqrstbcdefghaij
304 src_ptrs[i] = as_cu(src_buf._h_ptr) 1klmnopqrstbcdefghaij
305 dst_ptrs[i] = as_cu(dst_buf._h_ptr) 1klmnopqrstbcdefghaij
306 sizes[i] = src_buf.size 1klmnopqrstbcdefghaij
308 with nogil: 1klmnopqrstbcdefghaij
309 HANDLE_RETURN(cydriver.cuMemcpyBatchAsync( 1klmnopqrstbcdefghaij
310 dst_ptrs.data(),
311 src_ptrs.data(),
312 sizes.data(),
313 <size_t>n,
314 cu_attrs.data(),
315 attrs_idxs.data(),
316 num_attrs,
317 hstream,
318 ))