oxicuda_driver/stream_ordered_alloc.rs
1//! Stream-ordered memory allocation (CUDA 11.2+ / 12.x+).
2//!
3//! Stream-ordered memory allocation allows memory operations (`alloc` / `free`)
4//! to participate in the stream execution order, eliminating the need for
5//! explicit synchronisation between allocation and kernel launch.
6//!
7//! This module provides:
8//!
9//! * [`StreamMemoryPool`] — a memory pool bound to a specific device.
10//! * [`StreamAllocation`] — a handle to a stream-ordered allocation.
11//! * [`StreamOrderedAllocConfig`] — pool configuration (sizes, thresholds).
12//! * [`PoolAttribute`] / [`PoolUsageStats`] — attribute queries and statistics.
13//! * [`PoolExportDescriptor`] / [`ShareableHandleType`] — IPC sharing metadata.
14//! * [`stream_alloc`] / [`stream_free`] — convenience free functions.
15//!
16//! # The stream-ordered model
17//!
18//! Independently of the GPU driver, every pool carries a faithful CPU
19//! simulation of the stream-ordered allocator (see
20//! [`stream_ordered_model`](crate::stream_ordered_model)). Allocations
21//! (`alloc_async` / [`alloc_on`](StreamMemoryPool::alloc_on)) and frees
22//! (`free_async` / [`free_on`](StreamMemoryPool::free_on)) are sequenced per
23//! stream; freed blocks return to the pool once their stream reaches the free
24//! point and are then **reused** by a later same-or-larger request. This makes
25//! the allocator's semantics — visibility ordering, block reuse, and
26//! reserved-vs-used accounting — observable on a plain CPU, with no GPU
27//! required.
28//!
29//! # Platform behaviour
30//!
31//! On platforms with a real CUDA driver (Linux, Windows),
32//! [`StreamMemoryPool::new`] additionally creates a driver-side pool via
33//! `cuMemPoolCreate`. The lower-level `cuMem*Async` bindings remain available
34//! for real-GPU use. [`StreamMemoryPool::cpu_pool`] builds a pool backed only
35//! by the CPU model, so the full stream-ordered API can be exercised without a
36//! driver on any platform.
37//!
38//! # Example
39//!
40//! ```rust
41//! use oxicuda_driver::stream_ordered_alloc::*;
42//! use oxicuda_driver::StreamOrderId;
43//!
44//! // A pool backed by the faithful CPU model — no GPU driver required.
45//! let config = StreamOrderedAllocConfig::default_for_device(0);
46//! let mut pool = StreamMemoryPool::cpu_pool(config)?;
47//!
48//! // A genuine stream-ordering identity. In a GPU program this is derived
49//! // from a real `Stream` via `StreamMemoryPool::stream_id`; the model
50//! // sequences this token exactly like a real stream would.
51//! let stream = StreamOrderId::from(1);
52//!
53//! let mut alloc = pool.alloc_on(1024, stream)?;
54//! assert_eq!(alloc.size(), 1024);
55//! assert!(!alloc.is_freed());
56//!
57//! pool.free_on(&mut alloc, stream)?;
58//! assert!(alloc.is_freed());
59//! # Ok::<(), oxicuda_driver::CudaError>(())
60//! ```
61
62use std::fmt;
63
64use crate::error::{CudaError, CudaResult};
65use crate::ffi::CUdeviceptr;
66use crate::stream::Stream;
67use crate::stream_ordered_model::{ModelLimits, StreamOrderId, StreamOrderModel};
68
69// ---------------------------------------------------------------------------
70// Constants — CUmemPoolAttribute (mirrors CUDA header values)
71// ---------------------------------------------------------------------------
72
73/// Pool reuse policy: follow event dependencies.
74pub const CU_MEMPOOL_ATTR_REUSE_FOLLOW_EVENT_DEPENDENCIES: u32 = 1;
75/// Pool reuse policy: allow opportunistic reuse.
76pub const CU_MEMPOOL_ATTR_REUSE_ALLOW_OPPORTUNISTIC: u32 = 2;
77/// Pool reuse policy: allow internal dependency insertion.
78pub const CU_MEMPOOL_ATTR_REUSE_ALLOW_INTERNAL_DEPENDENCIES: u32 = 3;
79/// Release threshold in bytes (memory returned to OS when usage drops below).
80pub const CU_MEMPOOL_ATTR_RELEASE_THRESHOLD: u32 = 4;
81/// Current reserved memory (bytes) — read-only.
82pub const CU_MEMPOOL_ATTR_RESERVED_MEM_CURRENT: u32 = 5;
83/// High-water mark of reserved memory (bytes) — resettable.
84pub const CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH: u32 = 6;
85/// Current used memory (bytes) — read-only.
86pub const CU_MEMPOOL_ATTR_USED_MEM_CURRENT: u32 = 7;
87/// High-water mark of used memory (bytes) — resettable.
88pub const CU_MEMPOOL_ATTR_USED_MEM_HIGH: u32 = 8;
89
90// ---------------------------------------------------------------------------
91// StreamOrderedAllocConfig
92// ---------------------------------------------------------------------------
93
94/// Configuration for a stream-ordered memory pool.
95///
96/// All sizes are in bytes.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct StreamOrderedAllocConfig {
99 /// Initial pool size in bytes. The pool pre-reserves this amount of
100 /// device memory when created.
101 pub initial_pool_size: usize,
102
103 /// Maximum pool size in bytes. `0` means unlimited — the pool will grow
104 /// as needed (subject to device memory limits).
105 pub max_pool_size: usize,
106
107 /// Release threshold in bytes. When the pool is trimmed, at least this
108 /// much memory is kept reserved for future allocations.
109 pub release_threshold: usize,
110
111 /// The device ordinal to create the pool on.
112 pub device: i32,
113}
114
115impl StreamOrderedAllocConfig {
116 /// Validate that the configuration is internally consistent.
117 ///
118 /// # Rules
119 ///
120 /// * `initial_pool_size` must not exceed `max_pool_size` (when
121 /// `max_pool_size > 0`).
122 /// * `release_threshold` must not exceed `max_pool_size` (when
123 /// `max_pool_size > 0`).
124 /// * `device` must be non-negative.
125 ///
126 /// # Errors
127 ///
128 /// Returns [`CudaError::InvalidValue`] if any rule is violated.
129 pub fn validate(&self) -> CudaResult<()> {
130 if self.device < 0 {
131 return Err(CudaError::InvalidValue);
132 }
133
134 if self.max_pool_size > 0 {
135 if self.initial_pool_size > self.max_pool_size {
136 return Err(CudaError::InvalidValue);
137 }
138 if self.release_threshold > self.max_pool_size {
139 return Err(CudaError::InvalidValue);
140 }
141 }
142
143 Ok(())
144 }
145
146 /// Returns a sensible default configuration for the given device.
147 ///
148 /// * `initial_pool_size` = 0 (grow on demand)
149 /// * `max_pool_size` = 0 (unlimited)
150 /// * `release_threshold` = 0 (release everything on trim)
151 pub fn default_for_device(device: i32) -> Self {
152 Self {
153 initial_pool_size: 0,
154 max_pool_size: 0,
155 release_threshold: 0,
156 device,
157 }
158 }
159}
160
161// ---------------------------------------------------------------------------
162// PoolAttribute
163// ---------------------------------------------------------------------------
164
165/// Attributes that can be queried or set on a [`StreamMemoryPool`].
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum PoolAttribute {
168 /// Whether freed blocks can be reused by following event dependencies.
169 ReuseFollowEventDependencies,
170 /// Whether freed blocks can be opportunistically reused (without ordering).
171 ReuseAllowOpportunistic,
172 /// Whether the pool may insert internal dependencies for reuse.
173 ReuseAllowInternalDependencies,
174 /// The release threshold in bytes.
175 ReleaseThreshold(u64),
176 /// Current reserved memory (read-only query).
177 ReservedMemCurrent,
178 /// High-water mark of reserved memory.
179 ReservedMemHigh,
180 /// Current used memory (read-only query).
181 UsedMemCurrent,
182 /// High-water mark of used memory.
183 UsedMemHigh,
184}
185
186impl PoolAttribute {
187 /// Convert to the raw CUDA attribute constant.
188 pub fn to_raw(self) -> u32 {
189 match self {
190 Self::ReuseFollowEventDependencies => CU_MEMPOOL_ATTR_REUSE_FOLLOW_EVENT_DEPENDENCIES,
191 Self::ReuseAllowOpportunistic => CU_MEMPOOL_ATTR_REUSE_ALLOW_OPPORTUNISTIC,
192 Self::ReuseAllowInternalDependencies => {
193 CU_MEMPOOL_ATTR_REUSE_ALLOW_INTERNAL_DEPENDENCIES
194 }
195 Self::ReleaseThreshold(_) => CU_MEMPOOL_ATTR_RELEASE_THRESHOLD,
196 Self::ReservedMemCurrent => CU_MEMPOOL_ATTR_RESERVED_MEM_CURRENT,
197 Self::ReservedMemHigh => CU_MEMPOOL_ATTR_RESERVED_MEM_HIGH,
198 Self::UsedMemCurrent => CU_MEMPOOL_ATTR_USED_MEM_CURRENT,
199 Self::UsedMemHigh => CU_MEMPOOL_ATTR_USED_MEM_HIGH,
200 }
201 }
202}
203
204// ---------------------------------------------------------------------------
205// PoolUsageStats
206// ---------------------------------------------------------------------------
207
208/// Snapshot of pool memory usage.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
210pub struct PoolUsageStats {
211 /// Bytes currently reserved from the device allocator.
212 pub reserved_current: u64,
213 /// Peak bytes reserved (since creation or last reset).
214 pub reserved_high: u64,
215 /// Bytes currently in use by outstanding allocations.
216 pub used_current: u64,
217 /// Peak bytes in use (since creation or last reset).
218 pub used_high: u64,
219 /// Number of active (not-yet-freed) allocations.
220 pub active_allocations: usize,
221 /// Peak number of concurrent allocations.
222 pub peak_allocations: usize,
223}
224
225// ---------------------------------------------------------------------------
226// ShareableHandleType / PoolExportDescriptor
227// ---------------------------------------------------------------------------
228
229/// Handle type used for IPC sharing of memory pools.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
231pub enum ShareableHandleType {
232 /// No sharing.
233 #[default]
234 None,
235 /// POSIX file descriptor (Linux).
236 PosixFileDescriptor,
237 /// Win32 handle (Windows).
238 Win32Handle,
239 /// Win32 KMT handle (Windows, legacy).
240 Win32KmtHandle,
241}
242
243/// Descriptor for exporting a pool for IPC sharing.
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct PoolExportDescriptor {
246 /// The handle type to use for sharing.
247 pub shareable_handle_type: ShareableHandleType,
248 /// The device ordinal that owns the pool.
249 pub pool_device: i32,
250}
251
252// ---------------------------------------------------------------------------
253// StreamAllocation
254// ---------------------------------------------------------------------------
255
256/// Handle to a stream-ordered memory allocation.
257///
258/// An allocation lives on the GPU and is associated with a specific stream
259/// and memory pool. It becomes available when all preceding work on the
260/// stream has completed, and is returned to the pool when freed (also
261/// stream-ordered).
262pub struct StreamAllocation {
263 /// Device pointer (`CUdeviceptr`).
264 ptr: CUdeviceptr,
265 /// Size of the allocation in bytes.
266 size: usize,
267 /// The stream this allocation is ordered on (raw ordering token).
268 stream: u64,
269 /// The pool handle that owns this allocation.
270 pool: u64,
271 /// Sequence number at which the allocation becomes valid on its stream,
272 /// in the owning pool's [`StreamOrderModel`].
273 ready_seq: u64,
274 /// Whether this allocation has already been freed.
275 freed: bool,
276}
277
278impl StreamAllocation {
279 /// Returns the device pointer as a raw `u64` (`CUdeviceptr`).
280 #[inline]
281 pub fn as_ptr(&self) -> u64 {
282 self.ptr
283 }
284
285 /// Returns the allocation size in bytes.
286 #[inline]
287 pub fn size(&self) -> usize {
288 self.size
289 }
290
291 /// Returns `true` if this allocation has been freed.
292 #[inline]
293 pub fn is_freed(&self) -> bool {
294 self.freed
295 }
296
297 /// Returns the stream handle this allocation is ordered on.
298 #[inline]
299 pub fn stream(&self) -> u64 {
300 self.stream
301 }
302
303 /// Returns the ordering identifier of the stream this allocation is bound
304 /// to in the owning pool's stream-ordered model.
305 #[inline]
306 pub fn stream_id(&self) -> StreamOrderId {
307 StreamOrderId(self.stream)
308 }
309
310 /// Returns the sequence number at which this allocation becomes valid on
311 /// its stream within the owning pool's [`StreamOrderModel`].
312 ///
313 /// The allocation is safe to read on its own stream only once that stream
314 /// has executed past this point (queryable via
315 /// [`StreamMemoryPool::is_ready`]).
316 #[inline]
317 pub fn ready_seq(&self) -> u64 {
318 self.ready_seq
319 }
320
321 /// Returns the pool handle that owns this allocation.
322 #[inline]
323 pub fn pool(&self) -> u64 {
324 self.pool
325 }
326}
327
328impl fmt::Debug for StreamAllocation {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 f.debug_struct("StreamAllocation")
331 .field("ptr", &format_args!("0x{:016x}", self.ptr))
332 .field("size", &self.size)
333 .field("stream", &format_args!("0x{:016x}", self.stream))
334 .field("freed", &self.freed)
335 .finish()
336 }
337}
338
339// ---------------------------------------------------------------------------
340// StreamMemoryPool
341// ---------------------------------------------------------------------------
342
343/// A memory pool for stream-ordered allocations.
344///
345/// Every pool drives a faithful CPU model of the stream-ordered allocator (the
346/// source of truth for byte accounting, block reuse, and per-stream ordering).
347/// On platforms with a real CUDA driver (Linux, Windows),
348/// [`StreamMemoryPool::new`] *additionally* creates a driver-side pool via
349/// `cuMemPoolCreate`. [`StreamMemoryPool::cpu_pool`] builds a pool backed only
350/// by the CPU model and never touches the driver, so the API can be exercised
351/// on any platform.
352///
353/// # Allocation tracking
354///
355/// The pool maintains running allocation counts and byte totals (mirrored from
356/// the CPU model) for diagnostics; these are available everywhere via
357/// [`StreamMemoryPool::stats`].
358pub struct StreamMemoryPool {
359 /// Raw `CUmemoryPool` handle (0 if not backed by a real driver pool).
360 handle: u64,
361 /// Device ordinal.
362 device: i32,
363 /// Configuration used to create this pool.
364 config: StreamOrderedAllocConfig,
365 /// Number of currently active (not freed) allocations (mirror of the
366 /// model's live count, kept for cheap field access).
367 active_allocations: usize,
368 /// Total bytes currently in use (mirror of the model's `used`).
369 total_allocated: usize,
370 /// Peak bytes ever in use concurrently (mirror of the model's `used_high`).
371 peak_allocated: usize,
372 /// Peak number of concurrent allocations (mirror of the model's peak).
373 peak_allocation_count: usize,
374 /// Faithful CPU model of the stream-ordered allocator. This is the
375 /// authority for pointers, reuse, and stream ordering on every platform.
376 model: StreamOrderModel,
377}
378
379impl fmt::Debug for StreamMemoryPool {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 f.debug_struct("StreamMemoryPool")
382 .field("handle", &format_args!("0x{:016x}", self.handle))
383 .field("device", &self.device)
384 .field("active_allocations", &self.active_allocations)
385 .field("total_allocated", &self.total_allocated)
386 .field("reserved", &self.model.reserved())
387 .finish()
388 }
389}
390
391impl StreamMemoryPool {
392 /// Create a new memory pool for the given device.
393 ///
394 /// The configuration is validated and the CPU model is initialised. On
395 /// platforms with a real CUDA driver, `cuMemPoolCreate` is also invoked and
396 /// its handle stored; without a driver this fails cleanly.
397 ///
398 /// To obtain a pool that never touches the driver (e.g. for CPU-only use of
399 /// the stream-ordered API), use [`StreamMemoryPool::cpu_pool`].
400 ///
401 /// # Errors
402 ///
403 /// * [`CudaError::InvalidValue`] if the config fails validation.
404 /// * On non-macOS, any [`CudaError`] from `cuMemPoolCreate` (e.g.
405 /// [`CudaError::NotInitialized`] when no driver is loadable).
406 pub fn new(config: StreamOrderedAllocConfig) -> CudaResult<Self> {
407 config.validate()?;
408
409 #[cfg_attr(target_os = "macos", allow(unused_mut))]
410 let mut pool = Self::with_model(config);
411
412 // On real GPU platforms, create the driver-side pool via
413 // `cuMemPoolCreate` and store the returned handle. When the driver
414 // is absent the call returns `Err` and pool creation fails cleanly.
415 #[cfg(not(target_os = "macos"))]
416 {
417 pool.handle = Self::gpu_create_pool(&pool.config)?;
418 }
419
420 Ok(pool)
421 }
422
423 /// Create a pool backed solely by the faithful CPU model, without touching
424 /// the CUDA driver.
425 ///
426 /// This always succeeds (given a valid configuration) on every platform and
427 /// is the recommended entry point for using the stream-ordered allocation
428 /// semantics on a CPU.
429 ///
430 /// # Errors
431 ///
432 /// * [`CudaError::InvalidValue`] if the config fails validation.
433 pub fn cpu_pool(config: StreamOrderedAllocConfig) -> CudaResult<Self> {
434 config.validate()?;
435 Ok(Self::with_model(config))
436 }
437
438 /// Build a pool value with a fresh CPU model (no driver interaction).
439 fn with_model(config: StreamOrderedAllocConfig) -> Self {
440 let model = StreamOrderModel::new(Self::model_limits(&config));
441 Self {
442 handle: 0,
443 device: config.device,
444 config,
445 active_allocations: 0,
446 total_allocated: 0,
447 peak_allocated: 0,
448 peak_allocation_count: 0,
449 model,
450 }
451 }
452
453 /// Build the model limits from a pool configuration.
454 fn model_limits(config: &StreamOrderedAllocConfig) -> ModelLimits {
455 ModelLimits {
456 max_pool_size: config.max_pool_size,
457 release_threshold: config.release_threshold,
458 }
459 }
460
461 /// Derive a stream-ordering identifier from a genuine [`Stream`].
462 ///
463 /// The stream's raw handle is a stable, unique token for the lifetime of
464 /// the stream, which the CPU model uses as the stream's ordering identity.
465 #[inline]
466 pub fn stream_id(stream: &Stream) -> StreamOrderId {
467 StreamOrderId(stream.raw().0 as usize as u64)
468 }
469
470 /// Allocate memory on a stream (stream-ordered), identified by a raw
471 /// ordering token.
472 ///
473 /// The allocation becomes valid on the stream once the stream reaches the
474 /// allocation point. A previously-freed block of the same-or-larger size
475 /// is reused when available; otherwise a fresh block is carved from the
476 /// pool.
477 ///
478 /// # Errors
479 ///
480 /// * [`CudaError::InvalidValue`] if `size` is zero.
481 /// * [`CudaError::OutOfMemory`] if `max_pool_size` would be exceeded.
482 pub fn alloc_async(&mut self, size: usize, stream: u64) -> CudaResult<StreamAllocation> {
483 self.alloc_on(size, StreamOrderId(stream))
484 }
485
486 /// Allocate memory ordered on a genuine [`Stream`].
487 ///
488 /// This is the recommended entry point when a real CUDA [`Stream`] is
489 /// available: the allocation is sequenced against that exact stream in the
490 /// pool's [`StreamOrderModel`] (see [`StreamMemoryPool::stream_id`]).
491 ///
492 /// # Errors
493 ///
494 /// * [`CudaError::InvalidValue`] if `size` is zero.
495 /// * [`CudaError::OutOfMemory`] if `max_pool_size` would be exceeded.
496 pub fn alloc_async_on_stream(
497 &mut self,
498 size: usize,
499 stream: &Stream,
500 ) -> CudaResult<StreamAllocation> {
501 self.alloc_on(size, Self::stream_id(stream))
502 }
503
504 /// Allocate memory ordered on the stream identified by `stream`.
505 ///
506 /// The block is carved from the pool — reusing a previously-freed block of
507 /// the same-or-larger size when one is available — and sequenced on the
508 /// stream so that it only becomes valid once the stream reaches the
509 /// allocation point (queryable via [`StreamMemoryPool::is_ready`]).
510 ///
511 /// # Errors
512 ///
513 /// * [`CudaError::InvalidValue`] if `size` is zero.
514 /// * [`CudaError::OutOfMemory`] if `max_pool_size` would be exceeded.
515 pub fn alloc_on(&mut self, size: usize, stream: StreamOrderId) -> CudaResult<StreamAllocation> {
516 let model_alloc = self.model.alloc(size, stream)?;
517 self.sync_mirror_stats();
518
519 Ok(StreamAllocation {
520 ptr: model_alloc.ptr,
521 size: model_alloc.size,
522 stream: stream.raw(),
523 pool: self.handle,
524 ready_seq: model_alloc.ready_seq,
525 freed: false,
526 })
527 }
528
529 /// Free memory on a stream (stream-ordered).
530 ///
531 /// The memory is returned to the pool once all prior work on the
532 /// allocation's stream has completed. The allocation is marked freed and
533 /// cannot be freed again.
534 ///
535 /// # Errors
536 ///
537 /// * [`CudaError::InvalidValue`] if the allocation is already freed, or its
538 /// pointer is not live in this pool (foreign-pointer free).
539 pub fn free_async(&mut self, alloc: &mut StreamAllocation) -> CudaResult<()> {
540 let stream = alloc.stream_id();
541 self.free_on(alloc, stream)
542 }
543
544 /// Free `alloc` ordered on a genuine [`Stream`].
545 ///
546 /// CUDA permits freeing on a stream different from the one the allocation
547 /// was made on; the free still completes only once *that* stream reaches
548 /// the free point.
549 ///
550 /// # Errors
551 ///
552 /// * [`CudaError::InvalidValue`] if the allocation is already freed or its
553 /// pointer is not live in this pool.
554 pub fn free_async_on_stream(
555 &mut self,
556 alloc: &mut StreamAllocation,
557 stream: &Stream,
558 ) -> CudaResult<()> {
559 self.free_on(alloc, Self::stream_id(stream))
560 }
561
562 /// Free `alloc` ordered on the stream identified by `stream`.
563 ///
564 /// # Errors
565 ///
566 /// * [`CudaError::InvalidValue`] if the allocation is already freed or its
567 /// pointer is not live in this pool.
568 pub fn free_on(
569 &mut self,
570 alloc: &mut StreamAllocation,
571 stream: StreamOrderId,
572 ) -> CudaResult<()> {
573 if alloc.freed {
574 return Err(CudaError::InvalidValue);
575 }
576
577 self.model.free(alloc.ptr, stream)?;
578 self.sync_mirror_stats();
579
580 alloc.freed = true;
581 Ok(())
582 }
583
584 /// Advance a stream to its head (model of `cuStreamSynchronize`),
585 /// completing every operation submitted on it so far and reclaiming any
586 /// completed stream-ordered frees into the pool for reuse.
587 pub fn synchronize_stream(&mut self, stream: StreamOrderId) {
588 self.model.synchronize(stream);
589 self.sync_mirror_stats();
590 }
591
592 /// Returns `true` if `alloc` is valid to read on its own ordering stream,
593 /// i.e. that stream has executed past the allocation point.
594 pub fn is_ready(&self, alloc: &StreamAllocation) -> bool {
595 let model_alloc = crate::stream_ordered_model::ModelAllocation {
596 ptr: alloc.ptr,
597 size: alloc.size,
598 capacity: alloc.size,
599 stream: alloc.stream_id(),
600 ready_seq: alloc.ready_seq,
601 };
602 self.model.is_ready_same_stream(&model_alloc)
603 }
604
605 /// Returns `true` if `alloc` (made on its own stream) is safe to use on
606 /// `consumer` given that `consumer` was ordered after `wait_seq` on the
607 /// allocation's stream (the sequence captured by an event it waited on).
608 ///
609 /// Use [`StreamMemoryPool::record_event`] on the producing stream to obtain
610 /// a `wait_seq` that captures the allocation.
611 pub fn is_ready_on(
612 &self,
613 alloc: &StreamAllocation,
614 consumer: StreamOrderId,
615 wait_seq: u64,
616 ) -> bool {
617 let model_alloc = crate::stream_ordered_model::ModelAllocation {
618 ptr: alloc.ptr,
619 size: alloc.size,
620 capacity: alloc.size,
621 stream: alloc.stream_id(),
622 ready_seq: alloc.ready_seq,
623 };
624 self.model
625 .is_ready_cross_stream(&model_alloc, consumer, wait_seq)
626 }
627
628 /// Record an event on `stream`, returning the sequence number it captures.
629 ///
630 /// A later cross-stream wait on this value orders the waiting stream after
631 /// every operation submitted on `stream` before this point.
632 pub fn record_event(&mut self, stream: StreamOrderId) -> u64 {
633 self.model.record_event(stream)
634 }
635
636 /// Trim the CPU model's pool, releasing free-list bytes above
637 /// `min_bytes_to_keep` back to the (virtual) device.
638 ///
639 /// This is the CPU-model analogue of `cuMemPoolTrimTo`; it always succeeds
640 /// and is available on every platform. For the raw driver trim, see the
641 /// platform-gated [`StreamMemoryPool::trim`].
642 pub fn model_trim(&mut self, min_bytes_to_keep: usize) {
643 self.model.trim_to(min_bytes_to_keep);
644 self.sync_mirror_stats();
645 }
646
647 /// Trim the driver-side pool, releasing unused memory back to the OS.
648 ///
649 /// At least `min_bytes_to_keep` bytes of reserved memory remain in the
650 /// pool. This drives the real `cuMemPoolTrimTo` binding; for the
651 /// always-available CPU-model trim, use [`StreamMemoryPool::model_trim`].
652 ///
653 /// # Errors
654 ///
655 /// * [`CudaError::NotSupported`] on macOS.
656 /// * Any [`CudaError`] from `cuMemPoolTrimTo`.
657 pub fn trim(&mut self, min_bytes_to_keep: usize) -> CudaResult<()> {
658 self.platform_trim(min_bytes_to_keep)
659 }
660
661 /// Get pool usage statistics from the CPU model.
662 ///
663 /// `reserved_*` reflects everything the pool has carved from the (virtual)
664 /// device (live + reusable + pending-free bytes), whereas `used_*` reflects
665 /// only currently-live allocations.
666 pub fn stats(&self) -> PoolUsageStats {
667 PoolUsageStats {
668 reserved_current: self.model.reserved() as u64,
669 reserved_high: self.model.reserved_high() as u64,
670 used_current: self.model.used() as u64,
671 used_high: self.model.used_high() as u64,
672 active_allocations: self.model.active(),
673 peak_allocations: self.model.peak_active(),
674 }
675 }
676
677 /// Set a pool attribute.
678 ///
679 /// Only attributes that carry a value (e.g. [`PoolAttribute::ReleaseThreshold`])
680 /// modify pool state. Read-only attributes (e.g. `ReservedMemCurrent`)
681 /// return [`CudaError::InvalidValue`].
682 ///
683 /// The release threshold is applied to the CPU model as well as (on
684 /// non-macOS) the driver pool.
685 ///
686 /// # Errors
687 ///
688 /// * [`CudaError::InvalidValue`] for read-only attributes.
689 /// * [`CudaError::NotSupported`] on macOS for non-threshold attributes.
690 pub fn set_attribute(&mut self, attr: PoolAttribute) -> CudaResult<()> {
691 // Read-only attributes cannot be set.
692 match attr {
693 PoolAttribute::ReservedMemCurrent
694 | PoolAttribute::UsedMemCurrent
695 | PoolAttribute::ReservedMemHigh
696 | PoolAttribute::UsedMemHigh => {
697 return Err(CudaError::InvalidValue);
698 }
699 _ => {}
700 }
701
702 // Apply locally-meaningful attributes to the config and CPU model.
703 if let PoolAttribute::ReleaseThreshold(val) = attr {
704 self.config.release_threshold = val as usize;
705 self.model.set_release_threshold(val as usize);
706 }
707
708 self.platform_set_attribute(attr)
709 }
710
711 /// Enable peer access from another device to allocations in this pool.
712 ///
713 /// After this call, kernels running on `peer_device` can access memory
714 /// allocated from this pool.
715 ///
716 /// # Errors
717 ///
718 /// * [`CudaError::InvalidDevice`] if `peer_device` equals this pool's device.
719 /// * [`CudaError::NotSupported`] on macOS.
720 pub fn enable_peer_access(&self, peer_device: i32) -> CudaResult<()> {
721 if peer_device == self.device {
722 return Err(CudaError::InvalidDevice);
723 }
724
725 self.platform_enable_peer_access(peer_device)
726 }
727
728 /// Disable peer access from another device to allocations in this pool.
729 ///
730 /// # Errors
731 ///
732 /// * [`CudaError::InvalidDevice`] if `peer_device` equals this pool's device.
733 /// * [`CudaError::NotSupported`] on macOS.
734 pub fn disable_peer_access(&self, peer_device: i32) -> CudaResult<()> {
735 if peer_device == self.device {
736 return Err(CudaError::InvalidDevice);
737 }
738
739 self.platform_disable_peer_access(peer_device)
740 }
741
742 /// Reset peak statistics (peak used bytes and peak allocation count).
743 pub fn reset_peak_stats(&mut self) {
744 self.model.reset_peaks();
745 self.sync_mirror_stats();
746 }
747
748 /// Mirror the model's current/peak figures into the cheap struct fields.
749 fn sync_mirror_stats(&mut self) {
750 self.active_allocations = self.model.active();
751 self.total_allocated = self.model.used();
752 self.peak_allocated = self.model.used_high();
753 self.peak_allocation_count = self.model.peak_active();
754 }
755
756 /// Get the default memory pool for a device.
757 ///
758 /// CUDA provides a default pool per device, queried via
759 /// `cuDeviceGetDefaultMemPool`. The returned pool is owned by the
760 /// driver and is *not* destroyed when the [`StreamMemoryPool`] wrapper
761 /// is dropped. On macOS, this returns a local-only pool with default
762 /// configuration. In all cases the CPU model is initialised.
763 ///
764 /// # Errors
765 ///
766 /// * [`CudaError::InvalidValue`] if `device` is negative.
767 /// * [`CudaError::NotInitialized`] if the CUDA driver is not loaded.
768 /// * Any [`CudaError`] mapped from `cuDeviceGetDefaultMemPool`.
769 pub fn default_pool(device: i32) -> CudaResult<Self> {
770 if device < 0 {
771 return Err(CudaError::InvalidValue);
772 }
773
774 let config = StreamOrderedAllocConfig::default_for_device(device);
775
776 // On macOS there is no driver — fall back to a local-only pool.
777 #[cfg(target_os = "macos")]
778 {
779 Ok(Self::with_model(config))
780 }
781
782 // On real GPU platforms, resolve the device's default pool handle.
783 #[cfg(not(target_os = "macos"))]
784 {
785 let handle = Self::gpu_default_pool(device)?;
786 let mut pool = Self::with_model(config);
787 pool.handle = handle;
788 Ok(pool)
789 }
790 }
791
792 /// Returns the raw pool handle.
793 #[inline]
794 pub fn handle(&self) -> u64 {
795 self.handle
796 }
797
798 /// Returns the device ordinal.
799 #[inline]
800 pub fn device(&self) -> i32 {
801 self.device
802 }
803
804 /// Returns the pool configuration.
805 #[inline]
806 pub fn config(&self) -> &StreamOrderedAllocConfig {
807 &self.config
808 }
809
810 // -----------------------------------------------------------------------
811 // Platform-specific helpers (driver passthrough)
812 // -----------------------------------------------------------------------
813
814 /// Trim the driver pool on the current platform.
815 fn platform_trim(&mut self, min_bytes_to_keep: usize) -> CudaResult<()> {
816 #[cfg(target_os = "macos")]
817 {
818 let _ = min_bytes_to_keep;
819 Err(CudaError::NotSupported)
820 }
821
822 #[cfg(not(target_os = "macos"))]
823 {
824 Self::gpu_trim(self.handle, min_bytes_to_keep)
825 }
826 }
827
828 /// Set attribute on the driver pool on the current platform.
829 fn platform_set_attribute(&self, attr: PoolAttribute) -> CudaResult<()> {
830 #[cfg(target_os = "macos")]
831 {
832 match attr {
833 PoolAttribute::ReleaseThreshold(_) => Ok(()),
834 _ => Err(CudaError::NotSupported),
835 }
836 }
837
838 #[cfg(not(target_os = "macos"))]
839 {
840 Self::gpu_set_attribute(self.handle, attr)
841 }
842 }
843
844 /// Enable peer access on the driver pool on the current platform.
845 fn platform_enable_peer_access(&self, peer_device: i32) -> CudaResult<()> {
846 #[cfg(target_os = "macos")]
847 {
848 let _ = peer_device;
849 Err(CudaError::NotSupported)
850 }
851
852 #[cfg(not(target_os = "macos"))]
853 {
854 Self::gpu_enable_peer_access(self.handle, peer_device)
855 }
856 }
857
858 /// Disable peer access on the driver pool on the current platform.
859 fn platform_disable_peer_access(&self, peer_device: i32) -> CudaResult<()> {
860 #[cfg(target_os = "macos")]
861 {
862 let _ = peer_device;
863 Err(CudaError::NotSupported)
864 }
865
866 #[cfg(not(target_os = "macos"))]
867 {
868 Self::gpu_disable_peer_access(self.handle, peer_device)
869 }
870 }
871
872 // -----------------------------------------------------------------------
873 // GPU-only driver bindings (compiled out on macOS)
874 //
875 // These remain available for genuine GPU use. They are *not* on the CPU
876 // model's hot path: the model is the allocator on every platform, and these
877 // bindings are exercised directly by the `gpu_*` tests against whatever
878 // driver the host provides.
879 // -----------------------------------------------------------------------
880
881 /// Create the pool on the GPU via `cuMemPoolCreate`.
882 ///
883 /// Builds a [`CUmemPoolProps`] from the pool configuration (pinned device
884 /// memory on `config.device`, `max_size` from `config.max_pool_size`),
885 /// invokes the driver, and returns the raw `CUmemoryPool` handle encoded
886 /// as a `u64`.
887 ///
888 /// When the driver is absent, [`try_driver`](crate::loader::try_driver)
889 /// returns `Err(CudaError::NotInitialized)` and pool creation fails
890 /// cleanly. When the driver is present but predates CUDA 11.2 (no
891 /// `cuMemPoolCreate`), [`CudaError::NotSupported`] is returned.
892 #[cfg(not(target_os = "macos"))]
893 fn gpu_create_pool(config: &StreamOrderedAllocConfig) -> CudaResult<u64> {
894 use crate::ffi::{
895 CUmemAllocationType, CUmemLocation, CUmemLocationType, CUmemPoolProps, CUmemoryPool,
896 };
897
898 let api = crate::loader::try_driver()?;
899 let create = api.cu_mem_pool_create.ok_or(CudaError::NotSupported)?;
900
901 let props = CUmemPoolProps {
902 alloc_type: CUmemAllocationType::Pinned as u32,
903 handle_types: 0,
904 location: CUmemLocation {
905 loc_type: CUmemLocationType::Device as u32,
906 id: config.device,
907 },
908 win32_security_attributes: std::ptr::null_mut(),
909 max_size: config.max_pool_size,
910 reserved: [0u8; 56],
911 };
912
913 let mut pool = CUmemoryPool::default();
914 // SAFETY: `create` was just resolved from the driver; `props` and
915 // `pool` are valid, correctly-typed local variables, and the CUDA
916 // ABI's reserved padding is zeroed.
917 let rc = unsafe { create(&mut pool, &props) };
918 crate::error::check(rc)?;
919
920 Ok(pool.0 as usize as u64)
921 }
922
923 /// Resolve a device's default memory pool via `cuDeviceGetDefaultMemPool`.
924 #[cfg(not(target_os = "macos"))]
925 fn gpu_default_pool(device: i32) -> CudaResult<u64> {
926 use crate::ffi::CUmemoryPool;
927
928 let api = crate::loader::try_driver()?;
929 let get_default = api
930 .cu_device_get_default_mem_pool
931 .ok_or(CudaError::NotSupported)?;
932
933 let mut pool = CUmemoryPool::default();
934 // SAFETY: `get_default` was just resolved from the driver; `pool` is
935 // a valid local and `device` is a plain device ordinal.
936 let rc = unsafe { get_default(&mut pool, device) };
937 crate::error::check(rc)?;
938
939 Ok(pool.0 as usize as u64)
940 }
941
942 /// Allocate stream-ordered memory.
943 ///
944 /// When `pool_handle` is non-zero, allocates from that explicit pool via
945 /// `cuMemAllocFromPoolAsync`; when it is zero (default-pool semantics),
946 /// uses the context-wide `cuMemAllocAsync`.
947 ///
948 /// The CPU model is the allocator on every platform, so this real-driver
949 /// binding has no production caller; it is retained for genuine GPU use and
950 /// exercised directly by the `gpu_*` FFI tests. `#[cfg_attr(not(test), …)]`
951 /// keeps the production lib build warning-free without removing the
952 /// real binding.
953 #[cfg(not(target_os = "macos"))]
954 #[cfg_attr(not(test), allow(dead_code))]
955 fn gpu_alloc_async(pool_handle: u64, size: usize, stream: u64) -> CudaResult<CUdeviceptr> {
956 use crate::ffi::{CUmemoryPool, CUstream};
957
958 let api = crate::loader::try_driver()?;
959 let cu_stream = CUstream(stream as usize as *mut std::ffi::c_void);
960 let mut dptr: CUdeviceptr = 0;
961
962 if pool_handle != 0 {
963 let alloc_from_pool = api
964 .cu_mem_alloc_from_pool_async
965 .ok_or(CudaError::NotSupported)?;
966 let pool = CUmemoryPool(pool_handle as usize as *mut std::ffi::c_void);
967 // SAFETY: `alloc_from_pool` was just resolved; `dptr` is a valid
968 // out-pointer and `pool`/`cu_stream` are reconstructed handles.
969 let rc = unsafe { alloc_from_pool(&mut dptr, size, pool, cu_stream) };
970 crate::error::check(rc)?;
971 } else {
972 let alloc_async = api.cu_mem_alloc_async.ok_or(CudaError::NotSupported)?;
973 // SAFETY: `alloc_async` was just resolved; `dptr` is a valid
974 // out-pointer and `cu_stream` is a reconstructed handle.
975 let rc = unsafe { alloc_async(&mut dptr, size, cu_stream) };
976 crate::error::check(rc)?;
977 }
978
979 Ok(dptr)
980 }
981
982 /// Free stream-ordered memory via `cuMemFreeAsync`.
983 ///
984 /// Retained for genuine GPU use and exercised directly by the `gpu_*` FFI
985 /// tests; the CPU model is the allocator on the production path.
986 #[cfg(not(target_os = "macos"))]
987 #[cfg_attr(not(test), allow(dead_code))]
988 fn gpu_free_async(ptr: CUdeviceptr, stream: u64) -> CudaResult<()> {
989 use crate::ffi::CUstream;
990
991 let api = crate::loader::try_driver()?;
992 let free_async = api.cu_mem_free_async.ok_or(CudaError::NotSupported)?;
993 let cu_stream = CUstream(stream as usize as *mut std::ffi::c_void);
994 // SAFETY: `free_async` was just resolved from the driver; `ptr` is a
995 // device pointer previously returned by an async allocation and
996 // `cu_stream` is a reconstructed handle.
997 crate::error::check(unsafe { free_async(ptr, cu_stream) })
998 }
999
1000 /// Trim the pool via `cuMemPoolTrimTo`.
1001 #[cfg(not(target_os = "macos"))]
1002 fn gpu_trim(pool_handle: u64, min_bytes_to_keep: usize) -> CudaResult<()> {
1003 use crate::ffi::CUmemoryPool;
1004
1005 let api = crate::loader::try_driver()?;
1006 let trim = api.cu_mem_pool_trim_to.ok_or(CudaError::NotSupported)?;
1007 let pool = CUmemoryPool(pool_handle as usize as *mut std::ffi::c_void);
1008 // SAFETY: `trim` was just resolved from the driver; `pool` is a
1009 // reconstructed pool handle and `min_bytes_to_keep` is a plain count.
1010 crate::error::check(unsafe { trim(pool, min_bytes_to_keep) })
1011 }
1012
1013 /// Set a pool attribute via `cuMemPoolSetAttribute`.
1014 ///
1015 /// The reuse-policy attributes carry an `int` value; the release
1016 /// threshold carries a `cuuint64_t`. The value buffer is sized
1017 /// accordingly and passed to the driver.
1018 #[cfg(not(target_os = "macos"))]
1019 fn gpu_set_attribute(pool_handle: u64, attr: PoolAttribute) -> CudaResult<()> {
1020 use crate::ffi::CUmemoryPool;
1021
1022 let api = crate::loader::try_driver()?;
1023 let set_attr = api
1024 .cu_mem_pool_set_attribute
1025 .ok_or(CudaError::NotSupported)?;
1026 let pool = CUmemoryPool(pool_handle as usize as *mut std::ffi::c_void);
1027 let raw_attr = Self::map_pool_attribute(attr)?;
1028
1029 // The driver dereferences `value` as either `int` or `cuuint64_t`
1030 // depending on the attribute. Stack-allocate the correct width.
1031 match attr {
1032 PoolAttribute::ReuseFollowEventDependencies
1033 | PoolAttribute::ReuseAllowOpportunistic
1034 | PoolAttribute::ReuseAllowInternalDependencies => {
1035 // Boolean-style reuse policies: enable (1) the policy.
1036 let mut value: std::ffi::c_int = 1;
1037 // SAFETY: `set_attr` was just resolved; `pool` is a
1038 // reconstructed handle and `value` is a valid `int` matching
1039 // the attribute's documented value type.
1040 let rc = unsafe {
1041 set_attr(pool, raw_attr, (&mut value as *mut std::ffi::c_int).cast())
1042 };
1043 crate::error::check(rc)
1044 }
1045 PoolAttribute::ReleaseThreshold(threshold) => {
1046 let mut value: u64 = threshold;
1047 // SAFETY: `set_attr` was just resolved; `pool` is a
1048 // reconstructed handle and `value` is a valid `cuuint64_t`
1049 // matching the release-threshold value type.
1050 let rc = unsafe { set_attr(pool, raw_attr, (&mut value as *mut u64).cast()) };
1051 crate::error::check(rc)
1052 }
1053 // Read-only attributes are rejected before reaching this point.
1054 PoolAttribute::ReservedMemCurrent
1055 | PoolAttribute::ReservedMemHigh
1056 | PoolAttribute::UsedMemCurrent
1057 | PoolAttribute::UsedMemHigh => Err(CudaError::InvalidValue),
1058 }
1059 }
1060
1061 /// Map a [`PoolAttribute`] to the driver's [`CUmemPoolAttribute`].
1062 #[cfg(not(target_os = "macos"))]
1063 fn map_pool_attribute(attr: PoolAttribute) -> CudaResult<crate::ffi::CUmemPoolAttribute> {
1064 use crate::ffi::CUmemPoolAttribute;
1065 Ok(match attr {
1066 PoolAttribute::ReuseFollowEventDependencies => {
1067 CUmemPoolAttribute::ReuseFollowEventDependencies
1068 }
1069 PoolAttribute::ReuseAllowOpportunistic => CUmemPoolAttribute::ReuseAllowOpportunistic,
1070 PoolAttribute::ReuseAllowInternalDependencies => {
1071 CUmemPoolAttribute::ReuseAllowInternalDependencies
1072 }
1073 PoolAttribute::ReleaseThreshold(_) => CUmemPoolAttribute::ReleaseThreshold,
1074 PoolAttribute::ReservedMemCurrent => CUmemPoolAttribute::ReservedMemCurrent,
1075 PoolAttribute::ReservedMemHigh => CUmemPoolAttribute::ReservedMemHigh,
1076 PoolAttribute::UsedMemCurrent => CUmemPoolAttribute::UsedMemCurrent,
1077 PoolAttribute::UsedMemHigh => CUmemPoolAttribute::UsedMemHigh,
1078 })
1079 }
1080
1081 /// Enable peer access from `peer_device` via `cuMemPoolSetAccess`.
1082 ///
1083 /// Builds a [`CUmemAccessDesc`] granting read-write access to the peer
1084 /// device and applies it to the pool.
1085 #[cfg(not(target_os = "macos"))]
1086 fn gpu_enable_peer_access(pool_handle: u64, peer_device: i32) -> CudaResult<()> {
1087 Self::gpu_set_pool_access(pool_handle, peer_device, true)
1088 }
1089
1090 /// Disable peer access from `peer_device` via `cuMemPoolSetAccess`.
1091 #[cfg(not(target_os = "macos"))]
1092 fn gpu_disable_peer_access(pool_handle: u64, peer_device: i32) -> CudaResult<()> {
1093 Self::gpu_set_pool_access(pool_handle, peer_device, false)
1094 }
1095
1096 /// Shared implementation for enabling / disabling pool peer access.
1097 #[cfg(not(target_os = "macos"))]
1098 fn gpu_set_pool_access(pool_handle: u64, peer_device: i32, enable: bool) -> CudaResult<()> {
1099 use crate::ffi::{
1100 CUmemAccessDesc, CUmemAccessFlags, CUmemLocation, CUmemLocationType, CUmemoryPool,
1101 };
1102
1103 let api = crate::loader::try_driver()?;
1104 let set_access = api.cu_mem_pool_set_access.ok_or(CudaError::NotSupported)?;
1105 let pool = CUmemoryPool(pool_handle as usize as *mut std::ffi::c_void);
1106
1107 let flags = if enable {
1108 CUmemAccessFlags::ReadWrite
1109 } else {
1110 CUmemAccessFlags::None
1111 };
1112 let desc = CUmemAccessDesc {
1113 location: CUmemLocation {
1114 loc_type: CUmemLocationType::Device as u32,
1115 id: peer_device,
1116 },
1117 flags: flags as u32,
1118 };
1119
1120 // SAFETY: `set_access` was just resolved from the driver; `pool` is a
1121 // reconstructed handle and `desc` is a single valid descriptor.
1122 let rc = unsafe { set_access(pool, &desc, 1) };
1123 crate::error::check(rc)
1124 }
1125}
1126
1127// ---------------------------------------------------------------------------
1128// Convenience free functions
1129// ---------------------------------------------------------------------------
1130
1131/// Allocate memory on a stream using the default pool for device 0.
1132///
1133/// This is a convenience wrapper around [`StreamMemoryPool::default_pool`]
1134/// and [`StreamMemoryPool::alloc_async`].
1135///
1136/// # Errors
1137///
1138/// Propagates errors from pool creation and allocation.
1139pub fn stream_alloc(size: usize, stream: u64) -> CudaResult<StreamAllocation> {
1140 let mut pool = StreamMemoryPool::default_pool(0)?;
1141 pool.alloc_async(size, stream)
1142}
1143
1144/// Free a stream-ordered allocation.
1145///
1146/// Marks the allocation freed. This convenience function operates on the
1147/// allocation handle only (it does not require the owning pool); use
1148/// [`StreamMemoryPool::free_on`] when you need the freed block to re-enter a
1149/// specific pool's reuse list.
1150///
1151/// # Errors
1152///
1153/// * [`CudaError::InvalidValue`] if the allocation is already freed.
1154pub fn stream_free(alloc: &mut StreamAllocation) -> CudaResult<()> {
1155 if alloc.freed {
1156 return Err(CudaError::InvalidValue);
1157 }
1158
1159 alloc.freed = true;
1160 Ok(())
1161}
1162
1163// ---------------------------------------------------------------------------
1164// Tests
1165// ---------------------------------------------------------------------------
1166#[cfg(test)]
1167#[path = "stream_ordered_alloc_tests.rs"]
1168mod tests;