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

1# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4  

5from __future__ import annotations 

6  

7from collections.abc import Sequence 

8from typing import TYPE_CHECKING 

9  

10IF CUDA_CORE_BUILD_MAJOR >= 13: 

11 from libcpp.vector cimport vector 

12  

13from cuda.bindings cimport cydriver 

14from cuda.core._memory._buffer cimport Buffer, Buffer_check_open, Buffer_coerce_batch # no-cython-lint 

15  

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 

24  

25from cuda.core._host import Host 

26from cuda.core._utils.cuda_utils import driver 

27from cuda.core._memory._managed_location import _coerce_location 

28  

29if TYPE_CHECKING: 

30 from cuda.core._graph import GraphBuilder 

31 from cuda.core._device import Device 

32  

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")) 

36  

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} 

42  

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} 

51  

52  

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

59  

60  

61_SINGLE_MANAGED_HINT = "the ManagedBuffer instance method" 

62  

63  

64cdef inline tuple _coerce_batch_buffers(object buffers, str what): 

65 """Coerce ``buffers`` to a tuple[Buffer, ...]; rejects a single Buffer. 

66  

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

71  

72  

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

83  

84  

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 ) 

97  

98  

99def discard_batch(stream: Stream | GraphBuilder, buffers: Sequence[Buffer]) -> None: 

100 """Discard a batch of managed-memory ranges. 

101  

102 Requires CUDA 13+. For a single buffer, use 

103 :meth:`ManagedBuffer.discard` instead. 

104  

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. 

114  

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

122  

123 cdef Buffer buf 

124 for buf in bufs: 1a

125 _require_managed_buffer(buf, "discard_batch") 1a

126  

127 _do_batch_discard(bufs, s) 1a

128  

129  

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

138  

139  

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 ) 

162  

163  

164def _advise_one(Buffer buf, advice: driver.CUmem_advise, location: Device | Host | None) -> None: 

165 """Internal: apply managed-memory advice to a single buffer. 

166  

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

186  

187  

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)) 

208  

209  

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. 

216  

217 Requires CUDA 13+. For a single buffer, use 

218 :meth:`ManagedBuffer.prefetch` instead. 

219  

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)``. 

230  

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

241  

242 cdef Buffer buf 

243 for buf in bufs: 1acgh

244 _require_managed_buffer(buf, "prefetch_batch") 1acgh

245  

246 _do_batch_prefetch(bufs, locs, s) 1acgh

247  

248  

249def _do_single_prefetch_py(Buffer buf, location: Device | Host | None, stream: Stream | GraphBuilder | None) -> None: 

250 """Internal: single-buffer prefetch for ManagedBuffer.prefetch(). 

251  

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

258  

259  

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)) 

272  

273  

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 

282  

283  

284 def _read_preferred_location_v2(Buffer buf) -> Device | Host | None: 

285 """Internal: read preferred_location with full NUMA detail. 

286  

287 Bypasses cuda.bindings.driver.cuMemRangeGetAttribute (whose 

288 attribute allowlist doesn't yet include the cu13 _TYPE / _ID 

289 attributes) by calling cydriver directly. 

290  

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

319  

320  

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 ) 

359  

360  

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) 

372  

373  

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. 

380  

381 Requires CUDA 13+. For a single buffer, use 

382 :meth:`ManagedBuffer.discard_prefetch` instead. 

383  

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)``. 

394  

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

404  

405 cdef Buffer buf 

406 for buf in bufs: 1c

407 _require_managed_buffer(buf, "discard_prefetch_batch") 1c

408  

409 _do_batch_discard_prefetch(bufs, locs, s) 1c

410  

411  

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

421  

422  

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 )