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}