|
| 1 | +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | + |
| 5 | +from __future__ import annotations |
| 6 | + |
| 7 | +import dataclasses |
| 8 | +from collections.abc import Sequence |
| 9 | + |
| 10 | +from cuda.core._device import Device |
| 11 | +from cuda.core._host import Host |
| 12 | +from cuda.core._utils.cuda_utils import driver |
| 13 | +from cuda.core._utils.pycompat import StrEnum |
| 14 | +from cuda.core._utils.version import binding_version |
| 15 | + |
| 16 | +__all__ = ["CopyOptions", "MemcpyOverlapMode", "MemcpySrcAccessOrder"] |
| 17 | + |
| 18 | + |
| 19 | +class MemcpySrcAccessOrder(StrEnum): |
| 20 | + """Source access order hint for batched memcpy operations. |
| 21 | +
|
| 22 | + Maps to ``CUmemcpySrcAccessOrder``. |
| 23 | +
|
| 24 | + ``STREAM`` |
| 25 | + Source reads follow stream order. Earlier stream work may still be |
| 26 | + accessing the source when the copy is enqueued. |
| 27 | + ``DURING_API_CALL`` |
| 28 | + The driver may read the source out of stream order, but all reads |
| 29 | + are complete before :func:`copy_batch` returns. No earlier stream |
| 30 | + work may be accessing the source at the time of the call. |
| 31 | + ``ANY`` |
| 32 | + The driver may read the source after the call returns. The caller |
| 33 | + must keep the source unchanged until the copy completes in stream |
| 34 | + order. No earlier stream work may be accessing the source. |
| 35 | + """ |
| 36 | + |
| 37 | + STREAM = "stream" |
| 38 | + DURING_API_CALL = "during_api_call" |
| 39 | + ANY = "any" |
| 40 | + |
| 41 | + |
| 42 | +class MemcpyOverlapMode(StrEnum): |
| 43 | + """Overlap mode hint for batched memcpy operations. |
| 44 | +
|
| 45 | + Maps to ``CUmemcpyFlags``. |
| 46 | +
|
| 47 | + ``DEFAULT`` |
| 48 | + No overlap preference; the driver uses its default scheduling. |
| 49 | + ``PREFER_OVERLAP_WITH_COMPUTE`` |
| 50 | + Hint that the copy should preferably overlap with concurrent |
| 51 | + compute work. This is advisory and may be ignored depending on |
| 52 | + the platform and copy parameters. |
| 53 | + """ |
| 54 | + |
| 55 | + DEFAULT = "default" |
| 56 | + PREFER_OVERLAP_WITH_COMPUTE = "prefer_overlap_with_compute" |
| 57 | + |
| 58 | + |
| 59 | +@dataclasses.dataclass(frozen=True) |
| 60 | +class CopyOptions: |
| 61 | + """Attribute bundle for a single copy within a batched memcpy. |
| 62 | +
|
| 63 | + Parameters |
| 64 | + ---------- |
| 65 | + src_access_order : :class:`MemcpySrcAccessOrder` or str |
| 66 | + Hint describing how the source will be accessed. |
| 67 | + Default is ``"stream"`` (stream-ordered access). |
| 68 | + src_location_hint : :class:`cuda.core.Device` | :class:`cuda.core.Host` | None |
| 69 | + Hint for the source memory location. Honored only for managed |
| 70 | + memory on devices with concurrent managed access and for |
| 71 | + system-allocated pageable memory on devices with pageable memory |
| 72 | + access; ignored for all other memory types. Does not prefetch |
| 73 | + memory and does not set persistent memory advice. |
| 74 | + ``None`` means no hint. |
| 75 | + dst_location_hint : :class:`cuda.core.Device` | :class:`cuda.core.Host` | None |
| 76 | + Hint for the destination memory location. Same semantics and |
| 77 | + restrictions as ``src_location_hint``. ``None`` means no hint. |
| 78 | + overlap_mode : :class:`MemcpyOverlapMode` or str |
| 79 | + Hint requesting that the copy overlap with concurrent compute work. |
| 80 | + This is advisory; it has an effect only on devices that support it. |
| 81 | + Default is ``"default"``. |
| 82 | + """ |
| 83 | + |
| 84 | + src_access_order: MemcpySrcAccessOrder | str = "stream" |
| 85 | + src_location_hint: Device | Host | None = None |
| 86 | + dst_location_hint: Device | Host | None = None |
| 87 | + overlap_mode: MemcpyOverlapMode | str = "default" |
| 88 | + |
| 89 | + def __post_init__(self): |
| 90 | + # Frozen, unlike the other *Options dataclasses in cuda.core, because |
| 91 | + # the batched-API contract agreed in NVIDIA/cuda-python#1775 specifies |
| 92 | + # immutable per-call options: |
| 93 | + # https://github.com/NVIDIA/cuda-python/pull/1775#issuecomment-4355502334 |
| 94 | + # |
| 95 | + # Normalizing str -> StrEnum therefore has to go through |
| 96 | + # object.__setattr__; a plain assignment would raise |
| 97 | + # FrozenInstanceError. Done here rather than at use so that a typo |
| 98 | + # fails at construction and the field always holds the enum. |
| 99 | + if not isinstance(self.src_access_order, MemcpySrcAccessOrder): |
| 100 | + try: |
| 101 | + object.__setattr__( |
| 102 | + self, |
| 103 | + "src_access_order", |
| 104 | + MemcpySrcAccessOrder(self.src_access_order), |
| 105 | + ) |
| 106 | + except (ValueError, TypeError) as exc: |
| 107 | + raise ValueError(f"invalid src_access_order: {self.src_access_order!r}") from exc |
| 108 | + if not isinstance(self.overlap_mode, MemcpyOverlapMode): |
| 109 | + try: |
| 110 | + object.__setattr__( |
| 111 | + self, |
| 112 | + "overlap_mode", |
| 113 | + MemcpyOverlapMode(self.overlap_mode), |
| 114 | + ) |
| 115 | + except (ValueError, TypeError) as exc: |
| 116 | + raise ValueError(f"invalid overlap_mode: {self.overlap_mode!r}") from exc |
| 117 | + |
| 118 | + def _to_driver_enum(self) -> int: |
| 119 | + """Return the driver CUmemcpySrcAccessOrder value.""" |
| 120 | + if not _SRC_ACCESS_ORDER_TO_DRIVER: |
| 121 | + raise NotImplementedError(_CUDA13_REQUIRED) |
| 122 | + return _SRC_ACCESS_ORDER_TO_DRIVER[MemcpySrcAccessOrder(self.src_access_order)] |
| 123 | + |
| 124 | + def _to_driver_flags(self) -> int: |
| 125 | + """Return the driver CUmemcpyFlags value.""" |
| 126 | + if not _OVERLAP_MODE_TO_DRIVER: |
| 127 | + raise NotImplementedError(_CUDA13_REQUIRED) |
| 128 | + return _OVERLAP_MODE_TO_DRIVER[MemcpyOverlapMode(self.overlap_mode)] |
| 129 | + |
| 130 | + |
| 131 | +_CUDA13_REQUIRED = "copy attributes require cuda.bindings 13.0 or newer" |
| 132 | + |
| 133 | +# CUmemcpySrcAccessOrder and CUmemcpyFlags are exposed by cuda.bindings 13.0+, |
| 134 | +# so these maps are empty when it is older. Nothing reaches them there: |
| 135 | +# copy_batch refuses non-default CopyOptions when the batched entry point is |
| 136 | +# unavailable. |
| 137 | +# |
| 138 | +# Keyed by ``str``: under ``python_version = "3.10"`` mypy resolves StrEnum to |
| 139 | +# the unstubbed backports shim and so infers the members as plain ``str``. |
| 140 | +# StrEnum members are ``str`` instances, so this holds on every version. The |
| 141 | +# values are wrapped in ``int()`` because the driver enums are untyped. |
| 142 | +_SRC_ACCESS_ORDER_TO_DRIVER: dict[str, int] |
| 143 | +_OVERLAP_MODE_TO_DRIVER: dict[str, int] |
| 144 | + |
| 145 | +if binding_version() >= (13, 0, 0): |
| 146 | + _src_order = driver.CUmemcpySrcAccessOrder |
| 147 | + _flags = driver.CUmemcpyFlags |
| 148 | + _SRC_ACCESS_ORDER_TO_DRIVER = { |
| 149 | + MemcpySrcAccessOrder.STREAM: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_STREAM), |
| 150 | + MemcpySrcAccessOrder.DURING_API_CALL: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_DURING_API_CALL), |
| 151 | + MemcpySrcAccessOrder.ANY: int(_src_order.CU_MEMCPY_SRC_ACCESS_ORDER_ANY), |
| 152 | + } |
| 153 | + _OVERLAP_MODE_TO_DRIVER = { |
| 154 | + MemcpyOverlapMode.DEFAULT: int(_flags.CU_MEMCPY_FLAG_DEFAULT), |
| 155 | + MemcpyOverlapMode.PREFER_OVERLAP_WITH_COMPUTE: int(_flags.CU_MEMCPY_FLAG_PREFER_OVERLAP_WITH_COMPUTE), |
| 156 | + } |
| 157 | + del _src_order, _flags |
| 158 | +else: |
| 159 | + _SRC_ACCESS_ORDER_TO_DRIVER = {} |
| 160 | + _OVERLAP_MODE_TO_DRIVER = {} |
| 161 | + |
| 162 | + |
| 163 | +def _attr_run_starts(attrs: Sequence[CopyOptions]) -> list[int]: |
| 164 | + """Return the start index of each maximal run of equal attributes. |
| 165 | +
|
| 166 | + This mirrors the ``attrsIdxs`` indirection that ``cuMemcpyBatchAsync`` |
| 167 | + expects: ``attrs[k]`` applies to the copies in |
| 168 | + ``[starts[k], starts[k + 1])``. Collapsing equal neighbours means a |
| 169 | + broadcast attribute is passed to the driver once (``numAttrs == 1``) |
| 170 | + rather than repeated per copy. |
| 171 | + """ |
| 172 | + starts: list[int] = [] |
| 173 | + prev: CopyOptions | None = None |
| 174 | + for i, attr in enumerate(attrs): |
| 175 | + if i == 0 or attr != prev: |
| 176 | + starts.append(i) |
| 177 | + prev = attr |
| 178 | + return starts |
0 commit comments