@@ -27,13 +27,20 @@ from cuda.core._resource_handles cimport (
2727)
2828from cuda.core.typing import DevicePointerType
2929
30- from cuda.core._stream cimport Stream, Stream_accept, default_stream
30+ from cuda.core._memory._copy_attributes cimport _with_attributes_available
31+ from cuda.core._memory._copy_attributes cimport _to_cu_memcpy_attributes # no-cython-lint
32+
33+ IF CUDA_CORE_BUILD_MAJOR >= 13 :
34+ from cuda.core._resource_handles cimport memcpy_with_attributes_async
35+
36+ from cuda.core._stream cimport Stream, Stream_accept, Stream_is_legacy_default_token, default_stream
3137from cuda.core._utils.cuda_utils cimport HANDLE_RETURN, _parse_fill_value
3238
3339import sys
3440from collections.abc import Sequence
3541from typing import TYPE_CHECKING
3642
43+ from cuda.core._memory._copy_enums import CopyOptions, _reject_unsupported_during_api_call
3744from cuda.core._utils.pycompat import BufferProtocol
3845from cuda.core._dlpack import classify_dl_device, make_py_capsule
3946from cuda.core._device import Device
@@ -160,6 +167,75 @@ cdef inline void _init_memory_attrs(Buffer self):
160167 self ._mem_attrs_inited.store(True , memory_order_release)
161168
162169
170+ cdef bint _stream_is_capturing(Stream s):
171+ cdef cydriver.CUstreamCaptureStatus cap_status
172+ IF CUDA_CORE_BUILD_MAJOR >= 13 :
173+ HANDLE_RETURN(cydriver.cuStreamGetCaptureInfo(as_cu(s._h_stream), & cap_status,
174+ NULL , NULL , NULL , NULL , NULL ))
175+ ELSE :
176+ HANDLE_RETURN(cydriver.cuStreamGetCaptureInfo(as_cu(s._h_stream), & cap_status,
177+ NULL , NULL , NULL , NULL ))
178+ return cap_status == cydriver.CU_STREAM_CAPTURE_STATUS_ACTIVE
179+
180+
181+ cdef void _do_copy_with_attributes(
182+ cydriver.CUdeviceptr dst, cydriver.CUdeviceptr src, size_t nbytes,
183+ object options, cydriver.CUstream hstream,
184+ ):
185+ IF CUDA_CORE_BUILD_MAJOR >= 13 :
186+ # Routed through the memcpy_with_attributes_async() C++ shim since
187+ # cydriver.cuMemcpyWithAttributesAsync is absent from cuda-bindings < 13.2.
188+ cdef cydriver.CUmemcpyAttributes cu_attr = _to_cu_memcpy_attributes(options)
189+ with nogil:
190+ HANDLE_RETURN(memcpy_with_attributes_async(dst, src, nbytes, < void * > & cu_attr, hstream))
191+ ELSE :
192+ pass # unreachable: _with_attributes_available() is always False on CUDA 12
193+
194+
195+ cdef void _dispatch_buffer_copy(
196+ cydriver.CUdeviceptr dst, cydriver.CUdeviceptr src, size_t nbytes,
197+ Stream s, object options, str method_name,
198+ ):
199+ """ Submit a single copy, honoring CopyOptions when the attributes path is usable."""
200+ if options is None :
201+ with nogil:
202+ HANDLE_RETURN(cydriver.cuMemcpyAsync(dst, src, nbytes, as_cu(s._h_stream)))
203+ return
204+ if not isinstance (options, CopyOptions):
205+ raise TypeError (
206+ f" {method_name}: options must be CopyOptions, got {type(options).__name__}"
207+ )
208+ if Stream_is_legacy_default_token(s):
209+ raise TypeError (
210+ f" {method_name} does not accept LEGACY_DEFAULT_STREAM with options "
211+ " (matches copy_batch); cuMemcpyWithAttributesAsync rejects it outright, "
212+ " unlike PER_THREAD_DEFAULT_STREAM, which is a real stream to the driver "
213+ " and is accepted. Pass an explicit stream, PER_THREAD_DEFAULT_STREAM, "
214+ " or options=None."
215+ )
216+ if _stream_is_capturing(s):
217+ raise TypeError (
218+ f" {method_name} does not support graph capture with options "
219+ " (matches copy_batch); the driver has no graph-node form of "
220+ " cuMemcpyWithAttributesAsync, so options cannot be honored in a graph. "
221+ " Use GraphNode.memcpy for a plain (non-attributed) copy node, or pass "
222+ " options=None."
223+ )
224+ if _with_attributes_available():
225+ _do_copy_with_attributes(dst, src, nbytes, options, as_cu(s._h_stream))
226+ else :
227+ _reject_unsupported_during_api_call(
228+ options.src_access_order,
229+ " cuda.bindings and the driver to both report CUDA 13.2 or newer "
230+ " (cuMemcpyWithAttributesAsync is unavailable here)" ,
231+ )
232+ # STREAM and ANY never require access sooner than stream order, so
233+ # cuMemcpyAsync satisfies them; options are otherwise silently
234+ # ignored on this pre-CUDA-13.2 fallback path, matching copy_batch.
235+ with nogil:
236+ HANDLE_RETURN(cydriver.cuMemcpyAsync(dst, src, nbytes, as_cu(s._h_stream)))
237+
238+
163239cdef class Buffer:
164240 """ Represent a handle to allocated memory.
165241
@@ -396,7 +472,8 @@ cdef class Buffer:
396472 self .close()
397473 return False
398474
399- def copy_to (self , dst: Buffer | None = None , *, stream: Stream | GraphBuilder ) -> Buffer:
475+ def copy_to (self , dst: Buffer | None = None , *, stream: Stream | GraphBuilder ,
476+ options: CopyOptions | None = None ) -> Buffer:
400477 """Copy from this buffer to the dst buffer asynchronously on the given stream.
401478
402479 Copies the data from this buffer to the provided dst buffer.
@@ -411,6 +488,28 @@ cdef class Buffer:
411488 stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`
412489 Keyword argument specifying the stream for the
413490 asynchronous copy
491+ options : :class:`~utils.CopyOptions`, optional
492+ Transfer hints (source access order , location hints , overlap mode ).
493+ Honored when cuda.bindings and the driver are both CUDA 13.2 or
494+ newer. Not accepted with ``LEGACY_DEFAULT_STREAM``; use
495+ ``PER_THREAD_DEFAULT_STREAM`` instead. Not accepted with a
496+ capturing stream either , since a graph cannot represent these
497+ attributes; use :meth:`graph.GraphNode.memcpy` for a plain ,
498+ non-attributed copy node , or pass ``options = None ``. On an older
499+ cuda.bindings/driver , ``src_access_order`` values of ``STREAM``
500+ and ``ANY`` are silently ignored; ``DURING_API_CALL`` raises
501+ instead of silently downgrading its guarantee.
502+
503+ Raises
504+ ------
505+ TypeError
506+ If ``options`` is not a :class:`~utils.CopyOptions` instance , or
507+ if ``options`` is given together with ``LEGACY_DEFAULT_STREAM``
508+ or a stream currently in graph capture mode.
509+ RuntimeError
510+ If ``options.src_access_order`` is ``DURING_API_CALL`` and
511+ cuda.bindings or the driver is older than CUDA 13.2: falling
512+ back to a plain copy cannot honor that guarantee.
414513
415514 """
416515 Buffer_check_open(self )
@@ -430,12 +529,12 @@ cdef class Buffer:
430529 raise ValueError( "buffer sizes mismatch between src and dst (sizes "
431530 f"are: src = {src_size}, dst = {dst_size})"
432531 )
433- with nogil:
434- HANDLE_RETURN(cydriver.cuMemcpyAsync(
435- as_cu(dst._h_ptr ), as_cu(self._h_ptr ), src_size , as_cu(s._h_stream )))
532+ _dispatch_buffer_copy(
533+ as_cu(dst._h_ptr ), as_cu(self._h_ptr ), src_size , s , options , "copy_to")
436534 return dst
437535
438- def copy_from(self , src: Buffer , *, stream: Stream | GraphBuilder ) -> None:
536+ def copy_from(self , src: Buffer , *, stream: Stream | GraphBuilder ,
537+ options: CopyOptions | None = None ) -> None:
439538 """Copy from the src buffer to this buffer asynchronously on the given stream.
440539
441540 Parameters
@@ -445,7 +544,28 @@ cdef class Buffer:
445544 stream : :obj:`~_stream.Stream` | :obj:`~graph.GraphBuilder`
446545 Keyword argument specifying the stream for the
447546 asynchronous copy
547+ options : :class:`~utils.CopyOptions`, optional
548+ Transfer hints (source access order , location hints , overlap mode ).
549+ Honored when cuda.bindings and the driver are both CUDA 13.2 or
550+ newer. Not accepted with ``LEGACY_DEFAULT_STREAM``; use
551+ ``PER_THREAD_DEFAULT_STREAM`` instead. Not accepted with a
552+ capturing stream either , since a graph cannot represent these
553+ attributes; use :meth:`graph.GraphNode.memcpy` for a plain ,
554+ non-attributed copy node , or pass ``options = None ``. On an older
555+ cuda.bindings/driver , ``src_access_order`` values of ``STREAM``
556+ and ``ANY`` are silently ignored; ``DURING_API_CALL`` raises
557+ instead of silently downgrading its guarantee.
448558
559+ Raises
560+ ------
561+ TypeError
562+ If ``options`` is not a :class:`~utils.CopyOptions` instance , or
563+ if ``options`` is given together with ``LEGACY_DEFAULT_STREAM``
564+ or a stream currently in graph capture mode.
565+ RuntimeError
566+ If ``options.src_access_order`` is ``DURING_API_CALL`` and
567+ cuda.bindings or the driver is older than CUDA 13.2: falling
568+ back to a plain copy cannot honor that guarantee.
449569 """
450570 Buffer_check_open(self )
451571 Buffer_check_open(src )
@@ -457,9 +577,8 @@ cdef class Buffer:
457577 raise ValueError( "buffer sizes mismatch between src and dst (sizes "
458578 f"are: src = {src_size}, dst = {dst_size})"
459579 )
460- with nogil:
461- HANDLE_RETURN(cydriver.cuMemcpyAsync(
462- as_cu(self._h_ptr ), as_cu(src._h_ptr ), dst_size , as_cu(s._h_stream )))
580+ _dispatch_buffer_copy(
581+ as_cu(self._h_ptr ), as_cu(src._h_ptr ), dst_size , s , options , "copy_from")
463582
464583 def fill(self , value: int | BufferProtocol , *, stream: Stream | GraphBuilder ) -> None:
465584 """Fill this buffer with a repeating byte pattern.
0 commit comments