Skip to main content

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;