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}