Coverage for cuda/core/_memory/_managed_memory_ops.pyx: 94.84%
155 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 02:41 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 02:41 +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
8from typing import TYPE_CHECKING
10IF CUDA_CORE_BUILD_MAJOR >= 13:
11 from libcpp.vector cimport vector
13from cuda.bindings cimport cydriver
14from cuda.core._memory._buffer cimport Buffer, Buffer_check_open, Buffer_coerce_batch # no-cython-lint
16# to_cumemlocation / cumemlocation_from_id are referenced only from CUDA 13
17# branches. cython-lint does not evaluate compile-time IF blocks, so they
18# need a pragma to be seen as used.
19from cuda.core._memory._location cimport cumemlocation_from_id # no-cython-lint
20from cuda.core._memory._location cimport to_cumemlocation # no-cython-lint
21from cuda.core._resource_handles cimport as_cu
22from cuda.core._stream cimport Stream, Stream_accept
23from cuda.core._utils.cuda_utils cimport HANDLE_RETURN
25from cuda.core._host import Host
26from cuda.core._utils.cuda_utils import driver
27from cuda.core._memory._managed_location import _coerce_location
29if TYPE_CHECKING:
30 from cuda.core._graph import GraphBuilder
31 from cuda.core._device import Device
33cdef frozenset _ALL_LOCATION_TYPES = frozenset(("device", "host", "host_numa", "host_numa_current"))
34cdef frozenset _DEVICE_HOST_NUMA = frozenset(("device", "host", "host_numa"))
35cdef frozenset _DEVICE_HOST_ONLY = frozenset(("device", "host"))
37cdef set _ADVICE_IGNORES_LOCATION = {
38 driver.CUmem_advise.CU_MEM_ADVISE_SET_READ_MOSTLY,
39 driver.CUmem_advise.CU_MEM_ADVISE_UNSET_READ_MOSTLY,
40 driver.CUmem_advise.CU_MEM_ADVISE_UNSET_PREFERRED_LOCATION,
41}
43cdef dict _ADVICE_ALLOWED_LOCTYPES = {
44 driver.CUmem_advise.CU_MEM_ADVISE_SET_READ_MOSTLY: _DEVICE_HOST_NUMA,
45 driver.CUmem_advise.CU_MEM_ADVISE_UNSET_READ_MOSTLY: _DEVICE_HOST_NUMA,
46 driver.CUmem_advise.CU_MEM_ADVISE_SET_PREFERRED_LOCATION: _ALL_LOCATION_TYPES,
47 driver.CUmem_advise.CU_MEM_ADVISE_UNSET_PREFERRED_LOCATION: _DEVICE_HOST_NUMA,
48 driver.CUmem_advise.CU_MEM_ADVISE_SET_ACCESSED_BY: _DEVICE_HOST_ONLY,
49 driver.CUmem_advise.CU_MEM_ADVISE_UNSET_ACCESSED_BY: _DEVICE_HOST_ONLY,
50}
53cdef void _require_managed_buffer(Buffer self, str what):
54 # Buffer.is_managed handles both pointer-attribute and memory-resource
55 # paths (e.g. pool-allocated managed memory whose pointer attribute
56 # does not advertise CU_POINTER_ATTRIBUTE_IS_MANAGED).
57 if not self.is_managed: 1aclmnokijdstqrfepghw
58 raise ValueError(f"{what} requires a managed-memory allocation") 1w
61_SINGLE_MANAGED_HINT = "the ManagedBuffer instance method"
64cdef inline tuple _coerce_batch_buffers(object buffers, str what):
65 """Coerce ``buffers`` to a tuple[Buffer, ...]; rejects a single Buffer.
67 For single-buffer operations, use the corresponding ManagedBuffer
68 instance method instead.
69 """
70 return Buffer_coerce_batch(buffers, what, _SINGLE_MANAGED_HINT) 1acghuvxyz
73cdef tuple _broadcast_locations(object location, Py_ssize_t n, bint allow_none, str what):
74 if isinstance(location, Sequence): 1acghuv
75 if len(location) != n: 1guv
76 raise ValueError( 1uv
77 f"{what}: location length {len(location)} does not match " 1uv
78 f"targets length {n}" 1buv
79 )
80 return tuple(_coerce_location(loc, allow_none=allow_none) for loc in location) 1g
81 cdef object coerced = _coerce_location(location, allow_none=allow_none) 1ach
82 return tuple([coerced] * n) 1ach
85IF CUDA_CORE_BUILD_MAJOR < 13:
86 # CUDA 12 cuMemPrefetchAsync takes a device ordinal (-1 = host).
87 cdef inline int _to_legacy_device(object loc) except? -2:
88 cdef str kind = loc.kind
89 if kind == "device":
90 return <int>loc.id
91 if kind == "host":
92 return -1
93 raise RuntimeError(
94 "Host(numa_id=...) / Host.numa_current() require both cuda-bindings 13.0+ "
95 "and a CUDA 13+ runtime driver; use Host() instead"
96 )
99def discard_batch(stream: Stream | GraphBuilder, buffers: Sequence[Buffer]) -> None:
100 """Discard a batch of managed-memory ranges.
102 Requires CUDA 13+. For a single buffer, use
103 :meth:`ManagedBuffer.discard` instead.
105 Parameters
106 ----------
107 stream : :class:`~_stream.Stream` | :class:`~graph.GraphBuilder`
108 Stream for the asynchronous discard. First positional, required
109 (mirrors :func:`launch`).
110 buffers : Sequence[:class:`Buffer`]
111 Two or more managed allocations to discard. Resident pages are
112 released without prefetching new contents; subsequent access is
113 satisfied by lazy migration.
115 Raises
116 ------
117 NotImplementedError
118 On a CUDA 12 build of ``cuda.core``.
119 """
120 cdef tuple bufs = _coerce_batch_buffers(buffers, "discard_batch") 1ax
121 cdef Stream s = Stream_accept(stream) 1a
123 cdef Buffer buf
124 for buf in bufs: 1a
125 _require_managed_buffer(buf, "discard_batch") 1a
127 _do_batch_discard(bufs, s) 1a
130def _do_single_discard_py(Buffer buf, stream: Stream | GraphBuilder | None) -> None:
131 """Internal: single-buffer discard for ManagedBuffer.discard()."""
132 _require_managed_buffer(buf, "discard") 1ij
133 cdef Stream s = Stream_accept(stream) 1bij
134 # No single-range cuMemDiscard exists; route through the batched call
135 # with count=1.
136 cdef tuple bufs = (buf,) 1ij
137 _do_batch_discard(bufs, s) 1ij
140cdef void _do_batch_discard(tuple bufs, Stream s):
141 IF CUDA_CORE_BUILD_MAJOR >= 13:
142 cdef Py_ssize_t n = len(bufs) 1aij
143 cdef cydriver.CUstream hstream = as_cu(s._h_stream) 1aij
144 cdef vector[cydriver.CUdeviceptr] ptrs
145 cdef vector[size_t] sizes
146 ptrs.resize(n) 1aij
147 sizes.resize(n) 1aij
148 cdef Buffer buf
149 cdef Py_ssize_t i
150 for i in range(n): 1aij
151 buf = <Buffer>bufs[i] 1aij
152 ptrs[i] = as_cu(buf._h_ptr) 1aij
153 sizes[i] = buf._size 1aij
154 with nogil: 1aij
155 HANDLE_RETURN(cydriver.cuMemDiscardBatchAsync( 1aij
156 ptrs.data(), sizes.data(), <size_t>n, 0, hstream,
157 ))
158 ELSE:
159 raise NotImplementedError(
160 "discard requires a CUDA 13 build of cuda.core"
161 )
164def _advise_one(Buffer buf, advice: driver.CUmem_advise, location: Device | Host | None) -> None:
165 """Internal: apply managed-memory advice to a single buffer.
167 Used by :class:`ManagedBuffer` property setters. Not part of the
168 public API.
169 """
170 _require_managed_buffer(buf, "advise") 1lmnokqrfepw
171 if not isinstance(advice, driver.CUmem_advise): 1lmnokqrfep
172 raise TypeError(
173 f"advice must be a cuda.bindings.driver.CUmem_advise value, "
174 f"got {type(advice).__name__}"
175 )
176 cdef frozenset allowed_kinds = _ADVICE_ALLOWED_LOCTYPES.get(advice) 1lmnokqrfep
177 if allowed_kinds is None: 1lmnokqrfep
178 raise ValueError(f"Unsupported advice value: {advice!r}")
179 cdef bint allow_none = advice in _ADVICE_IGNORES_LOCATION 1lmnokqrfep
180 cdef object loc = _coerce_location(location, allow_none=allow_none) 1lmnokqrfep
181 if loc is not None and loc.kind not in allowed_kinds: 1lmnokqrfep
182 raise ValueError( 1kqr
183 f"advise {advice.name} does not support location_type='{loc.kind}'" 1kqr
184 )
185 _do_single_advise(buf, advice, loc, allow_none) 1lmnokfep
188cdef void _do_single_advise(Buffer buf, object advice_value, object loc, bint allow_none):
189 cdef cydriver.CUdeviceptr cu_ptr = as_cu(buf._h_ptr) 1lmnokfep
190 cdef size_t nbytes = buf._size 1lmnokfep
191 cdef cydriver.CUmem_advise advice_enum = <cydriver.CUmem_advise>(<int>int(advice_value)) 1lmnokfep
192 IF CUDA_CORE_BUILD_MAJOR >= 13:
193 cdef cydriver.CUmemLocation cu_loc
194 if loc is None: 1lmnokfep
195 # Driver ignores location for read_mostly / unset_preferred_location
196 # advice values but still validates the CUmemLocation; pass a
197 # host placeholder.
198 cu_loc = cumemlocation_from_id( 1kep
199 cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST, 0)
200 else:
201 cu_loc = to_cumemlocation(loc.kind, loc.id) 1lmnokfe
202 with nogil: 1lmnokfep
203 HANDLE_RETURN(cydriver.cuMemAdvise(cu_ptr, nbytes, advice_enum, cu_loc)) 1lmnokfep
204 ELSE:
205 cdef int dev_int = -1 if loc is None else _to_legacy_device(loc)
206 with nogil:
207 HANDLE_RETURN(cydriver.cuMemAdvise(cu_ptr, nbytes, advice_enum, dev_int))
210def prefetch_batch(
211 stream: Stream | GraphBuilder,
212 buffers: Sequence[Buffer],
213 locations: Device | Host | Sequence[Device | Host],
214) -> None:
215 """Prefetch a batch of managed-memory ranges to target locations.
217 Requires CUDA 13+. For a single buffer, use
218 :meth:`ManagedBuffer.prefetch` instead.
220 Parameters
221 ----------
222 stream : :class:`~_stream.Stream` | :class:`~graph.GraphBuilder`
223 Stream for the asynchronous prefetch. First positional, required
224 (mirrors :func:`launch`).
225 buffers : Sequence[:class:`Buffer`]
226 Two or more managed allocations to operate on.
227 locations : :class:`~cuda.core.Device` | :class:`~cuda.core.Host` | Sequence[...]
228 Target location(s). A single location applies to all buffers; a
229 sequence must match ``len(buffers)``.
231 Notes
232 -----
233 On a CUDA 12 build, falls back to a Python-level loop calling
234 ``cuMemPrefetchAsync`` per buffer (no batched driver entry point on
235 CUDA 12). CUDA 13 builds use ``cuMemPrefetchBatchAsync`` directly.
236 """
237 cdef tuple bufs = _coerce_batch_buffers(buffers, "prefetch_batch") 1acghvz
238 cdef Py_ssize_t n = len(bufs) 1acghv
239 cdef tuple locs = _broadcast_locations(locations, n, False, "prefetch_batch") 1acghv
240 cdef Stream s = Stream_accept(stream) 1acgh
242 cdef Buffer buf
243 for buf in bufs: 1acgh
244 _require_managed_buffer(buf, "prefetch_batch") 1acgh
246 _do_batch_prefetch(bufs, locs, s) 1acgh
249def _do_single_prefetch_py(Buffer buf, location: Device | Host | None, stream: Stream | GraphBuilder | None) -> None:
250 """Internal: single-buffer prefetch for ManagedBuffer.prefetch().
252 Uses cuMemPrefetchAsync (works on CUDA 12 and 13).
253 """
254 _require_managed_buffer(buf, "prefetch") 1ijdstqrw
255 cdef object loc = _coerce_location(location, allow_none=False) 1ijdstqr
256 cdef Stream s = Stream_accept(stream) 1ijdst
257 _do_single_prefetch(buf, loc, s) 1ijdst
260cdef void _do_single_prefetch(Buffer buf, object loc, Stream s):
261 cdef cydriver.CUdeviceptr cu_ptr = as_cu(buf._h_ptr) 1ijdst
262 cdef size_t nbytes = buf._size 1ijdst
263 cdef cydriver.CUstream hstream = as_cu(s._h_stream) 1ijdst
264 IF CUDA_CORE_BUILD_MAJOR >= 13:
265 cdef cydriver.CUmemLocation cu_loc = to_cumemlocation(loc.kind, loc.id) 1ijdst
266 with nogil: 1ijdst
267 HANDLE_RETURN(cydriver.cuMemPrefetchAsync(cu_ptr, nbytes, cu_loc, 0, hstream)) 1ijdst
268 ELSE:
269 cdef int dev_int = _to_legacy_device(loc)
270 with nogil:
271 HANDLE_RETURN(cydriver.cuMemPrefetchAsync(cu_ptr, nbytes, dev_int, hstream))
274IF CUDA_CORE_BUILD_MAJOR >= 13:
275 # Function-pointer type for cuMemPrefetchBatchAsync /
276 # cuMemDiscardAndPrefetchBatchAsync; both have identical signatures.
277 ctypedef cydriver.CUresult (*_BatchPrefetchFn)(
278 cydriver.CUdeviceptr*, size_t*, size_t,
279 cydriver.CUmemLocation*, size_t*, size_t,
280 unsigned long long, cydriver.CUstream,
281 ) except ?cydriver.CUDA_ERROR_NOT_FOUND nogil
284 def _read_preferred_location_v2(Buffer buf) -> Device | Host | None:
285 """Internal: read preferred_location with full NUMA detail.
287 Bypasses cuda.bindings.driver.cuMemRangeGetAttribute (whose
288 attribute allowlist doesn't yet include the cu13 _TYPE / _ID
289 attributes) by calling cydriver directly.
291 Returns Device | Host | None.
292 """
293 Buffer_check_open(buf) 1fe
294 cdef cydriver.CUdeviceptr cu_ptr = as_cu(buf._h_ptr) 1fe
295 cdef size_t nbytes = buf._size 1fe
296 cdef int loc_type = 0 1fe
297 cdef int loc_id = 0 1fe
298 with nogil: 1fe
299 HANDLE_RETURN(cydriver.cuMemRangeGetAttribute( 1fe
300 <void*>&loc_type, sizeof(int),
301 cydriver.CUmem_range_attribute.CU_MEM_RANGE_ATTRIBUTE_PREFERRED_LOCATION_TYPE,
302 cu_ptr, nbytes,
303 ))
304 HANDLE_RETURN(cydriver.cuMemRangeGetAttribute( 1fe
305 <void*>&loc_id, sizeof(int),
306 cydriver.CUmem_range_attribute.CU_MEM_RANGE_ATTRIBUTE_PREFERRED_LOCATION_ID,
307 cu_ptr, nbytes,
308 ))
309 if loc_type == <int>cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE: 1fe
310 from cuda.core._device import Device 1e
311 return Device(loc_id) 1e
312 if loc_type == <int>cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST: 1fe
313 return Host() 1e
314 if loc_type == <int>cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST_NUMA: 1fe
315 return Host(numa_id=loc_id)
316 if loc_type == <int>cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST_NUMA_CURRENT: 1fe
317 return Host.numa_current()
318 return None # CU_MEM_LOCATION_TYPE_INVALID — no preferred location 1fe
321 cdef void _do_batch_prefetch_op(tuple bufs, tuple locs, Stream s, _BatchPrefetchFn fn):
322 """Shared body for batched prefetch / discard-and-prefetch."""
323 cdef Py_ssize_t n = len(bufs) 1acdgh
324 cdef cydriver.CUstream hstream = as_cu(s._h_stream) 1acdgh
325 cdef vector[cydriver.CUdeviceptr] ptrs
326 cdef vector[size_t] sizes
327 cdef vector[cydriver.CUmemLocation] loc_arr
328 cdef vector[size_t] loc_indices
329 ptrs.resize(n) 1acdgh
330 sizes.resize(n) 1acdgh
331 loc_arr.resize(n) 1acdgh
332 loc_indices.resize(n) 1acdgh
333 cdef Buffer buf
334 cdef Py_ssize_t i
335 cdef object loc_spec
336 for i in range(n): 1acdgh
337 buf = <Buffer>bufs[i] 1acdgh
338 ptrs[i] = as_cu(buf._h_ptr) 1acdgh
339 sizes[i] = buf._size 1acdgh
340 loc_spec = locs[i] 1acdgh
341 loc_arr[i] = to_cumemlocation(loc_spec.kind, loc_spec.id) 1acdgh
342 loc_indices[i] = <size_t>i 1acdgh
343 with nogil: 1acdgh
344 HANDLE_RETURN(fn( 1acdgh
345 ptrs.data(), sizes.data(), <size_t>n,
346 loc_arr.data(), loc_indices.data(), <size_t>n,
347 0, hstream,
348 ))
349ELSE:
350 def _read_preferred_location_v2(Buffer buf) -> Device | Host | None:
351 # Symbol exists so _managed_buffer.py can `from ... import
352 # _read_preferred_location_v2` unconditionally at module top.
353 # `ManagedBuffer.preferred_location` gates on both
354 # binding_version() and driver_version() >= (13, 0, 0) before
355 # calling, so this path is unreachable on a cu12 build.
356 raise NotImplementedError(
357 "_read_preferred_location_v2 requires a CUDA 13 build of cuda.core"
358 )
361cdef void _do_batch_prefetch(tuple bufs, tuple locs, Stream s):
362 IF CUDA_CORE_BUILD_MAJOR >= 13:
363 _do_batch_prefetch_op(bufs, locs, s, cydriver.cuMemPrefetchBatchAsync) 1acgh
364 ELSE:
365 # cu12 has no cuMemPrefetchBatchAsync; loop per-range.
366 cdef Buffer buf
367 cdef Py_ssize_t i
368 cdef Py_ssize_t n = len(bufs)
369 for i in range(n):
370 buf = <Buffer>bufs[i]
371 _do_single_prefetch(buf, locs[i], s)
374def discard_prefetch_batch(
375 stream: Stream | GraphBuilder,
376 buffers: Sequence[Buffer],
377 locations: Device | Host | Sequence[Device | Host],
378) -> None:
379 """Discard a batch of managed-memory ranges and prefetch them to target locations.
381 Requires CUDA 13+. For a single buffer, use
382 :meth:`ManagedBuffer.discard_prefetch` instead.
384 Parameters
385 ----------
386 stream : :class:`~_stream.Stream` | :class:`~graph.GraphBuilder`
387 Stream for the asynchronous operation. First positional, required
388 (mirrors :func:`launch`).
389 buffers : Sequence[:class:`Buffer`]
390 Two or more managed allocations to discard and re-prefetch.
391 locations : :class:`~cuda.core.Device` | :class:`~cuda.core.Host` | Sequence[...]
392 Target location(s). A single location applies to all buffers;
393 a sequence must match ``len(buffers)``.
395 Raises
396 ------
397 NotImplementedError
398 On a CUDA 12 build of ``cuda.core``.
399 """
400 cdef tuple bufs = _coerce_batch_buffers(buffers, "discard_prefetch_batch") 1cuy
401 cdef Py_ssize_t n = len(bufs) 1cu
402 cdef tuple locs = _broadcast_locations(locations, n, False, "discard_prefetch_batch") 1cu
403 cdef Stream s = Stream_accept(stream) 1c
405 cdef Buffer buf
406 for buf in bufs: 1c
407 _require_managed_buffer(buf, "discard_prefetch_batch") 1c
409 _do_batch_discard_prefetch(bufs, locs, s) 1c
412def _do_single_discard_prefetch_py(Buffer buf, location: Device | Host | None, stream: Stream | GraphBuilder | None) -> None:
413 """Internal: single-buffer discard+prefetch for
414 ManagedBuffer.discard_prefetch()."""
415 _require_managed_buffer(buf, "discard_prefetch") 1dw
416 cdef object loc = _coerce_location(location, allow_none=False) 1d
417 cdef Stream s = Stream_accept(stream) 1d
418 cdef tuple bufs = (buf,) 1d
419 cdef tuple locs = (loc,) 1d
420 _do_batch_discard_prefetch(bufs, locs, s) 1d
423cdef void _do_batch_discard_prefetch(tuple bufs, tuple locs, Stream s):
424 IF CUDA_CORE_BUILD_MAJOR >= 13:
425 _do_batch_prefetch_op(bufs, locs, s, cydriver.cuMemDiscardAndPrefetchBatchAsync) 1cd
426 ELSE:
427 raise NotImplementedError(
428 "discard_prefetch requires a CUDA 13 build of cuda.core"
429 )