Skip to main content

oxicuda_memory/
memory_info.rs

1//! GPU memory usage queries and unified memory hints.
2//!
3//! This module provides:
4//!
5//! - [`MemoryInfo`] and [`memory_info`] for querying free/total GPU memory.
6//! - [`MemAdvice`] and [`mem_advise`] for providing memory usage hints to
7//!   the CUDA unified memory subsystem.
8//! - [`mem_prefetch`] for prefetching unified memory to a specific device.
9//!
10//! # Example
11//!
12//! ```rust,no_run
13//! # use oxicuda_memory::memory_info::{memory_info, MemoryInfo};
14//! let info = memory_info()?;
15//! println!("GPU memory: {} MB free / {} MB total",
16//!     info.free / (1024 * 1024),
17//!     info.total / (1024 * 1024),
18//! );
19//! # Ok::<(), oxicuda_driver::error::CudaError>(())
20//! ```
21
22use oxicuda_driver::device::Device;
23use oxicuda_driver::error::{CudaError, CudaResult};
24use oxicuda_driver::ffi::CUdevice;
25use oxicuda_driver::loader::try_driver;
26use oxicuda_driver::stream::Stream;
27
28// ---------------------------------------------------------------------------
29// MemoryInfo
30// ---------------------------------------------------------------------------
31
32/// GPU memory usage information.
33///
34/// Returned by [`memory_info`], this struct reports the free and total
35/// device memory for the current CUDA context.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct MemoryInfo {
38    /// Free device memory in bytes.
39    pub free: usize,
40    /// Total device memory in bytes.
41    pub total: usize,
42}
43
44impl MemoryInfo {
45    /// Returns the used memory in bytes (`total - free`).
46    #[inline]
47    pub fn used(&self) -> usize {
48        self.total.saturating_sub(self.free)
49    }
50
51    /// Returns the fraction of memory currently in use (0.0 to 1.0).
52    #[inline]
53    pub fn usage_fraction(&self) -> f64 {
54        if self.total == 0 {
55            return 0.0;
56        }
57        self.used() as f64 / self.total as f64
58    }
59}
60
61impl std::fmt::Display for MemoryInfo {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        write!(
64            f,
65            "MemoryInfo(free={} MB, total={} MB, used={:.1}%)",
66            self.free / (1024 * 1024),
67            self.total / (1024 * 1024),
68            self.usage_fraction() * 100.0,
69        )
70    }
71}
72
73/// Queries free and total device memory for the current CUDA context.
74///
75/// The returned values reflect the state at the time of the query and
76/// may change as other threads or processes allocate or free memory.
77///
78/// # Errors
79///
80/// Returns an error if no context is current or the driver call fails.
81pub fn memory_info() -> CudaResult<MemoryInfo> {
82    let driver = try_driver()?;
83    let mut free: usize = 0;
84    let mut total: usize = 0;
85    oxicuda_driver::check(unsafe { (driver.cu_mem_get_info_v2)(&mut free, &mut total) })?;
86    Ok(MemoryInfo { free, total })
87}
88
89// ---------------------------------------------------------------------------
90// MemAdvice
91// ---------------------------------------------------------------------------
92
93/// Memory advice hints for unified (managed) memory.
94///
95/// These hints guide the CUDA runtime's page migration and caching
96/// decisions for unified memory allocations. Providing accurate hints
97/// can significantly improve performance by reducing unnecessary page
98/// migrations.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100#[repr(u32)]
101pub enum MemAdvice {
102    /// Mark the memory region as read-mostly. This creates read-only
103    /// copies on accessing processors, reducing migration overhead.
104    SetReadMostly = 1,
105    /// Undo a previous `SetReadMostly` hint.
106    UnsetReadMostly = 2,
107    /// Set the preferred location for the memory region. The data will
108    /// preferably reside on the specified device.
109    SetPreferredLocation = 3,
110    /// Undo a previous `SetPreferredLocation` hint.
111    UnsetPreferredLocation = 4,
112    /// Indicate that the specified device will access this memory region.
113    /// This can cause the driver to create a mapping on that device.
114    SetAccessedBy = 5,
115    /// Undo a previous `SetAccessedBy` hint.
116    UnsetAccessedBy = 6,
117}
118
119/// Provides a memory usage hint for a unified memory region.
120///
121/// # Parameters
122///
123/// * `ptr` — device pointer to the start of the unified memory region.
124/// * `count` — size of the region in bytes.
125/// * `advice` — the usage hint to apply.
126/// * `device` — the device to which the hint applies.
127///
128/// # Errors
129///
130/// Returns an error if the pointer is not a managed allocation, the
131/// device is invalid, or the driver call fails.
132pub fn mem_advise(ptr: u64, count: usize, advice: MemAdvice, device: &Device) -> CudaResult<()> {
133    if count == 0 {
134        return Err(CudaError::InvalidValue);
135    }
136    let driver = try_driver()?;
137    oxicuda_driver::check(unsafe {
138        (driver.cu_mem_advise)(ptr, count, advice as u32, device.raw())
139    })
140}
141
142/// The CUDA driver's sentinel `CUdevice` value denoting host (CPU) memory
143/// (`CU_DEVICE_CPU` / `cudaCpuDeviceId`), for use with [`mem_advise_host`].
144const CU_DEVICE_CPU: CUdevice = -1;
145
146/// Provides a memory usage hint for a unified memory region, directing it
147/// at host (CPU) memory rather than a specific GPU device.
148///
149/// This is the counterpart to [`mem_advise`] for hints that target the CPU
150/// (e.g. [`MemAdvice::SetPreferredLocation`] when the caller wants data to
151/// remain resident in system memory) rather than any particular [`Device`].
152/// There is no [`Device`] value that represents the CPU, so this function
153/// passes the driver's special `CU_DEVICE_CPU` (`-1`) sentinel directly.
154///
155/// # Errors
156///
157/// Returns [`CudaError::InvalidValue`] if `count` is zero, or another
158/// [`CudaError`] if the driver call fails.
159pub fn mem_advise_host(ptr: u64, count: usize, advice: MemAdvice) -> CudaResult<()> {
160    if count == 0 {
161        return Err(CudaError::InvalidValue);
162    }
163    let driver = try_driver()?;
164    oxicuda_driver::check(unsafe {
165        (driver.cu_mem_advise)(ptr, count, advice as u32, CU_DEVICE_CPU)
166    })
167}
168
169/// Prefetches unified memory to the specified device.
170///
171/// This is an asynchronous operation enqueued on `stream`. The data
172/// is migrated to the target device so that subsequent accesses from
173/// that device will not cause page faults.
174///
175/// # Parameters
176///
177/// * `ptr` — device pointer to the start of the unified memory region.
178/// * `count` — size of the region in bytes.
179/// * `device` — the target device to prefetch to.
180/// * `stream` — the stream on which to enqueue the prefetch.
181///
182/// # Errors
183///
184/// Returns an error if the pointer is not a managed allocation, the
185/// device is invalid, or the driver call fails.
186pub fn mem_prefetch(ptr: u64, count: usize, device: &Device, stream: &Stream) -> CudaResult<()> {
187    if count == 0 {
188        return Err(CudaError::InvalidValue);
189    }
190    let driver = try_driver()?;
191    oxicuda_driver::check(unsafe {
192        (driver.cu_mem_prefetch_async)(ptr, count, device.raw(), stream.raw())
193    })
194}
195
196// ---------------------------------------------------------------------------
197// Tests
198// ---------------------------------------------------------------------------
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn memory_info_used_calculation() {
206        let info = MemoryInfo {
207            free: 4096,
208            total: 8192,
209        };
210        assert_eq!(info.used(), 4096);
211    }
212
213    #[test]
214    fn memory_info_usage_fraction() {
215        let info = MemoryInfo {
216            free: 2048,
217            total: 8192,
218        };
219        let frac = info.usage_fraction();
220        assert!((frac - 0.75).abs() < 1e-10);
221    }
222
223    #[test]
224    fn memory_info_usage_fraction_zero_total() {
225        let info = MemoryInfo { free: 0, total: 0 };
226        assert!((info.usage_fraction()).abs() < 1e-10);
227    }
228
229    #[test]
230    fn memory_info_display() {
231        let info = MemoryInfo {
232            free: 4 * 1024 * 1024,
233            total: 8 * 1024 * 1024,
234        };
235        let s = format!("{info}");
236        assert!(s.contains("free=4 MB"));
237        assert!(s.contains("total=8 MB"));
238    }
239
240    #[test]
241    fn mem_advice_variants() {
242        assert_eq!(MemAdvice::SetReadMostly as u32, 1);
243        assert_eq!(MemAdvice::UnsetReadMostly as u32, 2);
244        assert_eq!(MemAdvice::SetPreferredLocation as u32, 3);
245        assert_eq!(MemAdvice::UnsetPreferredLocation as u32, 4);
246        assert_eq!(MemAdvice::SetAccessedBy as u32, 5);
247        assert_eq!(MemAdvice::UnsetAccessedBy as u32, 6);
248    }
249
250    #[test]
251    fn mem_advise_rejects_zero_count() {
252        let dev = Device::get(0);
253        // On macOS we cannot get a device, so we test the zero-count path
254        // only if we can construct one.
255        if let Ok(dev) = dev {
256            let result = mem_advise(0x1000, 0, MemAdvice::SetReadMostly, &dev);
257            assert!(result.is_err());
258        }
259    }
260
261    #[test]
262    fn mem_prefetch_rejects_zero_count() {
263        // We cannot construct a Stream without a GPU context, but we can
264        // verify the function signature compiles.
265        let _: fn(u64, usize, &Device, &Stream) -> CudaResult<()> = mem_prefetch;
266    }
267
268    #[test]
269    fn mem_advise_host_rejects_zero_count() {
270        let result = mem_advise_host(0x1000, 0, MemAdvice::SetPreferredLocation);
271        assert_eq!(result, Err(CudaError::InvalidValue));
272    }
273
274    #[test]
275    fn mem_advise_host_signature_compiles() {
276        let _: fn(u64, usize, MemAdvice) -> CudaResult<()> = mem_advise_host;
277    }
278
279    #[test]
280    fn cu_device_cpu_is_negative_one() {
281        // `CU_DEVICE_CPU` (`cudaCpuDeviceId`) is a well-known CUDA driver
282        // sentinel; `mem_advise_host` relies on this exact value.
283        assert_eq!(CU_DEVICE_CPU, -1);
284    }
285
286    #[test]
287    fn memory_info_signature_compiles() {
288        let _: fn() -> CudaResult<MemoryInfo> = memory_info;
289    }
290}