Coverage for cuda/core/_memory/_managed_buffer.py: 44.44%
126 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# SPDX-License-Identifier: Apache-2.0
4from __future__ import annotations
6from collections.abc import Iterable, Iterator, MutableSet
7from typing import TYPE_CHECKING, Any
9from cuda.core._device import Device
10from cuda.core._host import Host
11from cuda.core._memory._buffer import Buffer
12from cuda.core._memory._managed_location import _coerce_location
13from cuda.core._memory._managed_memory_ops import (
14 _advise_one,
15 _do_single_discard_prefetch_py,
16 _do_single_discard_py,
17 _do_single_prefetch_py,
18 _read_last_prefetch_location_v2,
19 _read_preferred_location_v2,
20)
21from cuda.core._utils.cuda_utils import driver, handle_return
22from cuda.core._utils.version import BUILD_CUDA_MAJOR, driver_version
24if TYPE_CHECKING:
25 from cuda.core._memory._buffer import MemoryResource
26 from cuda.core._stream import Stream
27 from cuda.core.graph import GraphBuilder
29__all__ = ["ManagedBuffer"]
32_INT_SIZE = 4
34# Enum aliases — referenced once per property write, so cache the lookup.
35_ADV = driver.CUmem_advise
36_SET_READ_MOSTLY = _ADV.CU_MEM_ADVISE_SET_READ_MOSTLY
37_UNSET_READ_MOSTLY = _ADV.CU_MEM_ADVISE_UNSET_READ_MOSTLY
38_SET_PREFERRED = _ADV.CU_MEM_ADVISE_SET_PREFERRED_LOCATION
39_UNSET_PREFERRED = _ADV.CU_MEM_ADVISE_UNSET_PREFERRED_LOCATION
40_SET_ACCESSED_BY = _ADV.CU_MEM_ADVISE_SET_ACCESSED_BY
41_UNSET_ACCESSED_BY = _ADV.CU_MEM_ADVISE_UNSET_ACCESSED_BY
43_RANGE = driver.CUmem_range_attribute
44_ATTR_READ_MOSTLY = _RANGE.CU_MEM_RANGE_ATTRIBUTE_READ_MOSTLY
45_ATTR_PREFERRED = _RANGE.CU_MEM_RANGE_ATTRIBUTE_PREFERRED_LOCATION
46_ATTR_ACCESSED_BY = _RANGE.CU_MEM_RANGE_ATTRIBUTE_ACCESSED_BY
47_ATTR_LAST_PREFETCH = _RANGE.CU_MEM_RANGE_ATTRIBUTE_LAST_PREFETCH_LOCATION
50def _check_open(buf: Buffer) -> None:
51 if buf.is_closed: 1edabcgfhij
52 raise RuntimeError("Buffer has been closed") 1f
55def _get_int_attr(buf: Buffer, attribute: Any) -> int:
56 _check_open(buf) 1fj
57 return int(handle_return(driver.cuMemRangeGetAttribute(_INT_SIZE, attribute, buf.handle, buf.size))) 1j
60def _query_accessed_by(buf: Buffer) -> list[Device | Host]:
61 """Read the live ``CU_MEM_RANGE_ATTRIBUTE_ACCESSED_BY`` list.
63 Driver fills an int32 array: device id, ``-1`` = host, ``-2`` = empty.
64 Sized to ``cuDeviceGetCount() + 1`` (every visible device plus host).
65 """
66 _check_open(buf) 1edabcf
67 num_devices = handle_return(driver.cuDeviceGetCount()) 1edabc
68 n = num_devices + 1 1edabc
69 raw = handle_return(driver.cuMemRangeGetAttribute(n * _INT_SIZE, _ATTR_ACCESSED_BY, buf.handle, buf.size)) 1edabc
70 return [Host() if v == -1 else Device(v) for v in raw if v != -2] 1edabc
73class AccessedBySetProxy(MutableSet[Device | Host]):
74 """Live driver-backed view of ``set_accessed_by`` advice for a managed buffer.
76 Reads (``__contains__``, ``__iter__``, ``len(...)``) call
77 ``cuMemRangeGetAttribute``; writes (``add``, ``discard``) call
78 ``cuMemAdvise``. There is no in-memory mirror, so the view always
79 reflects the current driver state.
81 Note
82 ----
83 The driver returns integer device ordinals (``-1`` for host); host
84 NUMA distinctions applied via ``Host(numa_id=...)`` collapse to a
85 generic ``Host()`` when iterating this set.
86 """
88 __slots__ = ("_buf",)
90 def __init__(self, buf: ManagedBuffer):
91 self._buf = buf 1edabcgfhi
93 # Operators such as &|^ produce a plain set, not another proxy.
94 @classmethod
95 def _from_iterable(cls, it: Iterable[Any]) -> set[Device | Host]: # type: ignore[override]
96 return set(it) 1d
98 # --- abstract methods required by MutableSet ---
100 def __contains__(self, location: object) -> bool:
101 if not isinstance(location, (Device, Host)): 1edabc
102 return False 1a
103 return location in _query_accessed_by(self._buf) 1edbc
105 def __iter__(self) -> Iterator[Device | Host]:
106 return iter(_query_accessed_by(self._buf)) 1da
108 def __len__(self) -> int:
109 return len(_query_accessed_by(self._buf)) 1df
111 def add(self, location: Device | Host) -> None:
112 """Apply ``set_accessed_by`` advice for ``location``."""
113 if not isinstance(location, (Device, Host)): 1edaghi
114 raise TypeError(f"expected Device or Host, got {type(location).__name__}") 1a
115 _advise_one(self._buf, _SET_ACCESSED_BY, location) 1edghi
117 def discard(self, location: Device | Host) -> None:
118 """Apply ``unset_accessed_by`` advice for ``location``.
120 Per the ``MutableSet`` contract, ``discard`` is a no-op for elements
121 not in the set. ``set_accessed_by`` only accepts ``Device`` and the
122 generic ``Host()`` — NUMA-aware host variants (``Host(numa_id=...)``,
123 ``Host.numa_current()``) can never enter the set, so discarding them
124 is silently ignored rather than forwarded to the driver.
125 """
126 if not isinstance(location, (Device, Host)): 1eda
127 return 1a
128 if isinstance(location, Host) and (location.numa_id is not None or location.is_numa_current): 1ed
129 return 1d
130 _advise_one(self._buf, _UNSET_ACCESSED_BY, location) 1ed
132 def __repr__(self) -> str:
133 return f"AccessedBySetProxy({set(_query_accessed_by(self._buf))!r})" 1d
136class ManagedBuffer(Buffer):
137 """Managed (unified) memory buffer with a property-style advice API.
139 Returned by :meth:`ManagedMemoryResource.allocate`, or wrap an
140 existing managed-memory pointer with :meth:`ManagedBuffer.from_handle`.
142 Examples
143 --------
144 >>> buf = mr.allocate(size)
145 >>> buf.read_mostly = True
146 >>> buf.preferred_location = Device(0)
147 >>> buf.accessed_by.add(Device(1))
148 >>> buf.prefetch(Device(0), stream=stream)
150 Note
151 ----
152 On CUDA 13 builds, ``preferred_location`` and
153 ``last_prefetch_location`` round-trip full NUMA information. On CUDA 12
154 builds, ``Host(numa_id=...)`` and ``Host.numa_current()`` are rejected
155 with ``TypeError`` at the call boundary — only ``Device(...)`` and the
156 generic ``Host()`` are accepted. Use ``Host()`` to target the host on
157 CUDA 12.
158 """
160 @classmethod
161 def from_handle(
162 cls,
163 ptr,
164 size: int,
165 mr: MemoryResource | None = None,
166 owner: object | None = None,
167 *,
168 stream: Stream | GraphBuilder | None = None,
169 ) -> Buffer:
170 """Wrap an existing managed-memory pointer in a :class:`ManagedBuffer`.
172 Use this when you have an externally-allocated managed pointer
173 and want the property-style advice API (:attr:`read_mostly`,
174 :attr:`preferred_location`, :attr:`last_prefetch_location`,
175 :attr:`accessed_by`).
177 Parameters
178 ----------
179 ptr : :obj:`~_memory.DevicePointerT`
180 Pointer to a managed allocation.
181 size : int
182 Allocation size in bytes.
183 mr : :obj:`~_memory.MemoryResource`, optional
184 Memory resource that owns ``ptr``. When provided, its
185 ``deallocate`` is called when the buffer is closed.
186 owner : object, optional
187 An object that keeps the underlying allocation alive.
188 ``owner`` and ``mr`` cannot both be specified.
189 stream : Stream | GraphBuilder, optional
190 Keyword-only. The stream used to order the buffer's deallocation
191 when ``mr`` owns the pointer. Defaults to ``default_stream()``.
192 Recording a default-stream token requires a CUDA context to be
193 current. If the buffer may be freed from a different host thread,
194 pass a stream other than the per-thread default stream, which
195 refers to a different stream on each thread.
196 """
197 return cls._init(ptr, size, mr=mr, owner=owner, stream=stream) 1yzABCDEFGHIfJkKLmMNOPQ
199 @property
200 def read_mostly(self) -> bool:
201 """Whether ``set_read_mostly`` advice is currently applied."""
202 return _get_int_attr(self, _ATTR_READ_MOSTLY) != 0 1fj
204 @read_mostly.setter
205 def read_mostly(self, value: bool) -> None:
206 _advise_one(self, _SET_READ_MOSTLY if value else _UNSET_READ_MOSTLY, None) 1gjm
208 @property
209 def preferred_location(self) -> Device | Host | None:
210 """Currently applied ``set_preferred_location`` target, or ``None``.
212 On CUDA 13 builds, fully round-trips ``Host(numa_id=N)``. On CUDA 12
213 the legacy attribute carries only a device ordinal (or ``-1`` for
214 host), so ``Host(numa_id=N)`` set via the setter round-trips back
215 as ``Host()``.
216 """
217 # The v2 path uses CU_MEM_RANGE_ATTRIBUTE_PREFERRED_LOCATION_{TYPE,ID},
218 # both added in CUDA 13. The path exists in the CUDA 13 build only, and
219 # the runtime driver must be 13.0+. Otherwise fall back to the legacy
220 # device-ordinal path. See PR #2054 / #2064 for prior regressions.
221 if BUILD_CUDA_MAJOR >= 13 and driver_version() >= (13, 0, 0): 1fkl
222 return _read_preferred_location_v2(self) 1fkl
223 # CUDA 12 legacy path. No NUMA info is available. A CUDA 13 build also
224 # takes this path when the runtime driver is still 12.x.
225 loc_id = _get_int_attr(self, _ATTR_PREFERRED)
226 if loc_id == -2:
227 return None
228 if loc_id == -1:
229 return Host()
230 return Device(loc_id)
232 @preferred_location.setter
233 def preferred_location(self, value: Device | Host | None) -> None:
234 if value is None: 1gkl
235 _advise_one(self, _UNSET_PREFERRED, None) 1l
236 else:
237 _advise_one(self, _SET_PREFERRED, value) 1gkl
239 @property
240 def last_prefetch_location(self) -> Device | Host | None:
241 """Location targeted by the most recent explicit prefetch.
243 Returns ``None`` if any page in the range has never been prefetched,
244 or if the pages do not share one last-prefetch location. This reports
245 the application's requested destination, not current residency or
246 completion of the asynchronous prefetch operation.
248 On CUDA 13 builds, fully round-trips ``Host(numa_id=N)``. On CUDA 12,
249 the legacy attribute carries only a device ordinal (or ``-1`` for
250 host), so host NUMA details are unavailable.
251 """
252 if BUILD_CUDA_MAJOR >= 13 and driver_version() >= (13, 0, 0): 1snoptqrfuv
253 return _read_last_prefetch_location_v2(self) 1snoptqrfuv
254 loc_id = _get_int_attr(self, _ATTR_LAST_PREFETCH)
255 if loc_id == -2:
256 return None
257 if loc_id == -1:
258 return Host()
259 return Device(loc_id)
261 @property
262 def accessed_by(self) -> AccessedBySetProxy:
263 """Live set-like view of ``set_accessed_by`` locations."""
264 _check_open(self) 1edabcgfhi
265 return AccessedBySetProxy(self) 1edabcgfhi
267 @accessed_by.setter
268 def accessed_by(self, locations: Iterable[Device | Host]) -> None:
269 _check_open(self) 1abc
270 # Validate every target before issuing any cuMemAdvise so an invalid
271 # element can't leave accessed_by partially mutated.
272 target: set[Device | Host] = set() 1abc
273 for loc in locations: 1abc
274 if not isinstance(loc, (Device, Host)): 1abc
275 raise TypeError(f"accessed_by entries must be Device or Host, got {type(loc).__name__}") 1a
276 target.add(loc) 1abc
277 for loc in target: 1abc
278 spec = _coerce_location(loc) 1abc
279 assert spec is not None 1abc
280 if spec.kind not in ("device", "host"): 1abc
281 raise ValueError(f"advise {_SET_ACCESSED_BY.name} does not support location_type='{spec.kind}'") 1b
282 current = set(_query_accessed_by(self)) 1abc
283 for loc in current - target: 1abc
284 _advise_one(self, _UNSET_ACCESSED_BY, loc) 1c
285 for loc in target - current: 1abc
286 _advise_one(self, _SET_ACCESSED_BY, loc) 1abc
288 def prefetch(self, location: Device | Host, *, stream: Stream | GraphBuilder) -> None:
289 """Prefetch this range to ``location`` on ``stream``."""
290 _do_single_prefetch_py(self, location, stream) 1wxnopqrhim
292 def discard(self, *, stream: Stream | GraphBuilder) -> None:
293 """Discard this range's resident pages on ``stream`` (CUDA 13+)."""
294 _do_single_discard_py(self, stream) 1wx
296 def discard_prefetch(self, location: Device | Host, *, stream: Stream | GraphBuilder) -> None:
297 """Discard this range and prefetch to ``location`` on ``stream`` (CUDA 13+)."""
298 _do_single_discard_prefetch_py(self, location, stream) 1nm