Skip to main content

oxicuda_memory/
unified.rs

1//! Unified (managed) memory buffer.
2//!
3//! [`UnifiedBuffer<T>`] wraps `cuMemAllocManaged`, which allocates memory
4//! that is automatically migrated between host and device by the CUDA
5//! Unified Memory subsystem.  The allocation is accessible from both CPU
6//! code (via [`as_slice`](UnifiedBuffer::as_slice) /
7//! [`as_mut_slice`](UnifiedBuffer::as_mut_slice)) and GPU kernels (via
8//! [`as_device_ptr`](UnifiedBuffer::as_device_ptr)).
9//!
10//! # Coherence caveat
11//!
12//! The host-side accessors are only safe to call when no GPU kernel is
13//! concurrently reading or writing the same memory.  After launching a
14//! kernel that touches a unified buffer, synchronise the stream (or the
15//! entire context) before accessing the data from the host.
16//!
17//! # Ownership
18//!
19//! The allocation is freed with `cuMemFree_v2` on drop.  Errors during
20//! drop are logged via [`tracing::warn`].
21//!
22//! # Example
23//!
24//! ```rust,no_run
25//! # use oxicuda_memory::UnifiedBuffer;
26//! let mut ubuf = UnifiedBuffer::<f32>::alloc(512)?;
27//! // Write from the host side (no kernel running).
28//! for (i, v) in ubuf.as_mut_slice().iter_mut().enumerate() {
29//!     *v = i as f32;
30//! }
31//! // Pass ubuf.as_device_ptr() to a kernel…
32//! # Ok::<(), oxicuda_driver::error::CudaError>(())
33//! ```
34
35use std::marker::PhantomData;
36
37use oxicuda_driver::error::{CudaError, CudaResult};
38use oxicuda_driver::ffi::{CU_MEM_ATTACH_GLOBAL, CUdeviceptr};
39use oxicuda_driver::loader::try_driver;
40
41// ---------------------------------------------------------------------------
42// UnifiedBuffer<T>
43// ---------------------------------------------------------------------------
44
45/// A contiguous buffer of `T` elements in CUDA unified (managed) memory.
46///
47/// Unified memory is accessible from both the host CPU and the GPU device.
48/// The CUDA driver transparently migrates pages between host and device as
49/// needed.  This simplifies programming at the cost of potential migration
50/// overhead compared to explicit device buffers.
51pub struct UnifiedBuffer<T: Copy> {
52    /// The CUDA device pointer.  For managed memory this value is also a
53    /// valid host pointer (on 64-bit systems with UVA).
54    ptr: CUdeviceptr,
55    /// Host-accessible pointer derived from `ptr`.
56    host_ptr: *mut T,
57    /// Number of `T` elements (not bytes).
58    len: usize,
59    /// Marker to tie the generic parameter `T` to this struct.
60    _phantom: PhantomData<T>,
61}
62
63// SAFETY: Unified memory is accessible from any thread on both host and
64// device.  Proper synchronisation is the caller's responsibility.
65unsafe impl<T: Copy + Send> Send for UnifiedBuffer<T> {}
66unsafe impl<T: Copy + Sync> Sync for UnifiedBuffer<T> {}
67
68impl<T: Copy> UnifiedBuffer<T> {
69    /// Allocates a unified memory buffer capable of holding `n` elements of
70    /// type `T`.
71    ///
72    /// The memory is allocated with [`CU_MEM_ATTACH_GLOBAL`], making it
73    /// accessible from any stream on any device in the system.
74    ///
75    /// # Errors
76    ///
77    /// * [`CudaError::InvalidValue`] if `n` is zero.
78    /// * [`CudaError::OutOfMemory`] if the allocation fails.
79    /// * Other driver errors from `cuMemAllocManaged`.
80    pub fn alloc(n: usize) -> CudaResult<Self> {
81        if n == 0 {
82            return Err(CudaError::InvalidValue);
83        }
84        let byte_size = n
85            .checked_mul(std::mem::size_of::<T>())
86            .ok_or(CudaError::InvalidValue)?;
87        let api = try_driver()?;
88        let mut dev_ptr: CUdeviceptr = 0;
89        // SAFETY: `cu_mem_alloc_managed` writes a valid device pointer that
90        // is also host-accessible (UVA).
91        let rc =
92            unsafe { (api.cu_mem_alloc_managed)(&mut dev_ptr, byte_size, CU_MEM_ATTACH_GLOBAL) };
93        oxicuda_driver::check(rc)?;
94        // On 64-bit systems with UVA, the device pointer value is the same
95        // as the host virtual address.
96        let host_ptr = dev_ptr as *mut T;
97        // SAFETY: `host_ptr` is the host-accessible address of the managed
98        // allocation just created above, valid for `byte_size` bytes; no
99        // kernel has had a chance to touch it yet, so writing to it from the
100        // host is legal. Zero-initialising here means `as_slice`/
101        // `as_mut_slice` below never expose driver-uninitialised memory as
102        // a safe `&[T]` (all-zero bytes are a valid `T` for every in-tree
103        // usage of `UnifiedBuffer`, e.g. `u8`/`f32`). Skipped when `T` is
104        // zero-sized (byte_size == 0 despite `n > 0`) to avoid a no-op
105        // write through a possibly-unusual pointer.
106        if byte_size > 0 {
107            unsafe {
108                std::ptr::write_bytes(host_ptr.cast::<u8>(), 0, byte_size);
109            }
110        }
111        Ok(Self {
112            ptr: dev_ptr,
113            host_ptr,
114            len: n,
115            _phantom: PhantomData,
116        })
117    }
118
119    /// Returns the number of `T` elements in this buffer.
120    #[inline]
121    pub fn len(&self) -> usize {
122        self.len
123    }
124
125    /// Returns `true` if the buffer contains zero elements.
126    #[inline]
127    pub fn is_empty(&self) -> bool {
128        self.len == 0
129    }
130
131    /// Returns the total size of the allocation in bytes.
132    #[inline]
133    pub fn byte_size(&self) -> usize {
134        self.len * std::mem::size_of::<T>()
135    }
136
137    /// Returns the raw [`CUdeviceptr`] handle for use in kernel launches
138    /// and other device-side operations.
139    #[inline]
140    pub fn as_device_ptr(&self) -> CUdeviceptr {
141        self.ptr
142    }
143
144    /// Returns a shared slice over the buffer's host-accessible contents.
145    ///
146    /// # Safety note
147    ///
148    /// This is only safe to call when no GPU kernel is concurrently
149    /// reading or writing this buffer.  Synchronise the relevant stream
150    /// or context before calling this method.
151    #[inline]
152    pub fn as_slice(&self) -> &[T] {
153        // SAFETY: `host_ptr` is valid for `len` elements, zero-initialised
154        // at `alloc` time, when no device kernel is concurrently accessing
155        // the memory.  The caller is responsible for proper synchronisation.
156        unsafe { std::slice::from_raw_parts(self.host_ptr, self.len) }
157    }
158
159    /// Returns a mutable slice over the buffer's host-accessible contents.
160    ///
161    /// # Safety note
162    ///
163    /// This is only safe to call when no GPU kernel is concurrently
164    /// reading or writing this buffer.  Synchronise the relevant stream
165    /// or context before calling this method.
166    #[inline]
167    pub fn as_mut_slice(&mut self) -> &mut [T] {
168        // SAFETY: `host_ptr` is valid for `len` elements, zero-initialised
169        // at `alloc` time, when no device kernel is concurrently accessing
170        // the memory.  The caller is responsible for proper synchronisation.
171        unsafe { std::slice::from_raw_parts_mut(self.host_ptr, self.len) }
172    }
173}
174
175impl<T: Copy> Drop for UnifiedBuffer<T> {
176    fn drop(&mut self) {
177        if let Ok(api) = try_driver() {
178            // SAFETY: `self.ptr` was allocated by `cu_mem_alloc_managed`
179            // and has not yet been freed.
180            let rc = unsafe { (api.cu_mem_free_v2)(self.ptr) };
181            if rc != 0 {
182                tracing::warn!(
183                    cuda_error = rc,
184                    ptr = self.ptr,
185                    len = self.len,
186                    "cuMemFree_v2 failed during UnifiedBuffer drop"
187                );
188            }
189        }
190    }
191}
192
193// ---------------------------------------------------------------------------
194// Tests
195// ---------------------------------------------------------------------------
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn alloc_signature_compiles() {
203        let _: fn(usize) -> CudaResult<UnifiedBuffer<f32>> = UnifiedBuffer::alloc;
204    }
205
206    #[cfg(feature = "gpu-tests")]
207    mod gpu_tests {
208        use super::*;
209
210        /// Establishes a real CUDA context on device 0. Returns `None` if no
211        /// driver/GPU is available so tests can skip gracefully.
212        fn real_context() -> Option<oxicuda_driver::context::Context> {
213            if oxicuda_driver::init().is_err()
214                || oxicuda_driver::device::Device::count().unwrap_or(0) == 0
215            {
216                return None;
217            }
218            let dev = oxicuda_driver::device::Device::get(0).ok()?;
219            oxicuda_driver::context::Context::new(&dev).ok()
220        }
221
222        /// Regression test for F070: a freshly allocated `UnifiedBuffer`
223        /// must never expose driver-uninitialised bytes through the safe
224        /// `as_slice` accessor — it must read back as all-zero.
225        #[test]
226        fn alloc_is_zero_initialized() {
227            let Some(_ctx) = real_context() else {
228                eprintln!("skipping: no CUDA driver/device");
229                return;
230            };
231            let Ok(buf) = UnifiedBuffer::<u8>::alloc(4096) else {
232                eprintln!("skipping: alloc failed");
233                return;
234            };
235            assert_eq!(buf.len(), 4096);
236            assert!(buf.as_slice().iter().all(|&b| b == 0));
237        }
238
239        #[test]
240        fn as_mut_slice_writes_are_visible() {
241            let Some(_ctx) = real_context() else {
242                eprintln!("skipping: no CUDA driver/device");
243                return;
244            };
245            let Ok(mut buf) = UnifiedBuffer::<f32>::alloc(64) else {
246                eprintln!("skipping: alloc failed");
247                return;
248            };
249            for (i, v) in buf.as_mut_slice().iter_mut().enumerate() {
250                *v = i as f32;
251            }
252            let expected: Vec<f32> = (0..64).map(|i| i as f32).collect();
253            assert_eq!(buf.as_slice(), expected.as_slice());
254        }
255    }
256}