oxicuda_memory/host_buffer.rs
1//! Pinned (page-locked) host memory buffer.
2//!
3//! [`PinnedBuffer<T>`] allocates host memory via `cuMemAllocHost_v2`, which
4//! pins the pages so that the CUDA driver can perform DMA transfers without
5//! an intermediate staging copy. This is the recommended source/destination
6//! for asynchronous host-device transfers.
7//!
8//! # Deref
9//!
10//! `PinnedBuffer<T>` implements [`Deref`] and [`DerefMut`] to `[T]`, so it
11//! can be used anywhere a slice is expected.
12//!
13//! # Ownership
14//!
15//! The allocation is freed with `cuMemFreeHost` on drop. Errors during
16//! drop are logged via [`tracing::warn`].
17//!
18//! # Example
19//!
20//! ```rust,no_run
21//! # use oxicuda_memory::PinnedBuffer;
22//! let mut pinned = PinnedBuffer::<f32>::alloc(256)?;
23//! for (i, v) in pinned.iter_mut().enumerate() {
24//! *v = i as f32;
25//! }
26//! assert_eq!(pinned.len(), 256);
27//! # Ok::<(), oxicuda_driver::error::CudaError>(())
28//! ```
29
30use std::ffi::c_void;
31use std::ops::{Deref, DerefMut};
32
33use oxicuda_driver::error::{CudaError, CudaResult};
34use oxicuda_driver::loader::try_driver;
35
36// ---------------------------------------------------------------------------
37// PinnedBuffer<T>
38// ---------------------------------------------------------------------------
39
40/// A contiguous buffer of `T` elements in page-locked (pinned) host memory.
41///
42/// Pinned memory enables the CUDA driver to use DMA for host-device
43/// transfers, avoiding an extra copy through a staging buffer. This makes
44/// pinned buffers the preferred choice for async copy operations.
45///
46/// The buffer dereferences to `&[T]` / `&mut [T]` for ergonomic access.
47pub struct PinnedBuffer<T: Copy> {
48 /// Pointer to the start of the pinned allocation.
49 ptr: *mut T,
50 /// Number of `T` elements (not bytes).
51 len: usize,
52}
53
54// SAFETY: The pinned host memory is not thread-local; it is a plain heap
55// allocation that is safe to access from any thread.
56unsafe impl<T: Copy + Send> Send for PinnedBuffer<T> {}
57unsafe impl<T: Copy + Sync> Sync for PinnedBuffer<T> {}
58
59impl<T: Copy> PinnedBuffer<T> {
60 /// Allocates a pinned host buffer capable of holding `n` elements of type `T`.
61 ///
62 /// # Errors
63 ///
64 /// * [`CudaError::InvalidValue`] if `n` is zero.
65 /// * [`CudaError::OutOfMemory`] if the host cannot satisfy the request.
66 /// * Other driver errors from `cuMemAllocHost_v2`.
67 pub fn alloc(n: usize) -> CudaResult<Self> {
68 if n == 0 {
69 return Err(CudaError::InvalidValue);
70 }
71 let byte_size = n
72 .checked_mul(std::mem::size_of::<T>())
73 .ok_or(CudaError::InvalidValue)?;
74 let api = try_driver()?;
75 let mut raw_ptr: *mut c_void = std::ptr::null_mut();
76 // SAFETY: `cu_mem_alloc_host_v2` writes a valid host pointer on success.
77 let rc = unsafe { (api.cu_mem_alloc_host_v2)(&mut raw_ptr, byte_size) };
78 oxicuda_driver::check(rc)?;
79 // SAFETY: `raw_ptr` was just allocated by `cu_mem_alloc_host_v2` and
80 // is valid for `byte_size` bytes; zero-initialising it here means
81 // `as_slice`/`as_mut_slice` below never expose driver-uninitialised
82 // memory as a safe `&[T]` (all-zero bytes are a valid `T` for every
83 // in-tree usage of `PinnedBuffer`, e.g. `u8`/`f32`). Skipped when
84 // `T` is zero-sized (byte_size == 0 despite `n > 0`) to avoid a
85 // no-op write through a possibly-unusual pointer.
86 if byte_size > 0 {
87 unsafe {
88 std::ptr::write_bytes(raw_ptr.cast::<u8>(), 0, byte_size);
89 }
90 }
91 Ok(Self {
92 ptr: raw_ptr.cast::<T>(),
93 len: n,
94 })
95 }
96
97 /// Allocates a pinned host buffer and copies the contents of `data` into it.
98 ///
99 /// # Errors
100 ///
101 /// * [`CudaError::InvalidValue`] if `data` is empty.
102 /// * Other driver errors from allocation.
103 pub fn from_slice(data: &[T]) -> CudaResult<Self> {
104 let buf = Self::alloc(data.len())?;
105 // SAFETY: both `data` and `buf.ptr` point to valid memory of
106 // `data.len() * size_of::<T>()` bytes, and `T: Copy`.
107 unsafe {
108 std::ptr::copy_nonoverlapping(data.as_ptr(), buf.ptr, data.len());
109 }
110 Ok(buf)
111 }
112
113 /// Returns the number of `T` elements in this buffer.
114 #[inline]
115 pub fn len(&self) -> usize {
116 self.len
117 }
118
119 /// Returns `true` if the buffer contains zero elements.
120 #[inline]
121 pub fn is_empty(&self) -> bool {
122 self.len == 0
123 }
124
125 /// Returns a raw const pointer to the buffer's data.
126 #[inline]
127 pub fn as_ptr(&self) -> *const T {
128 self.ptr
129 }
130
131 /// Returns a raw mutable pointer to the buffer's data.
132 #[inline]
133 pub fn as_mut_ptr(&mut self) -> *mut T {
134 self.ptr
135 }
136
137 /// Returns a shared slice over the buffer's contents.
138 #[inline]
139 pub fn as_slice(&self) -> &[T] {
140 // SAFETY: `self.ptr` is a valid, aligned allocation of `self.len`
141 // elements, zero-initialised at `alloc` time, and we have `&self` so
142 // no mutable alias exists.
143 unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
144 }
145
146 /// Returns a mutable slice over the buffer's contents.
147 #[inline]
148 pub fn as_mut_slice(&mut self) -> &mut [T] {
149 // SAFETY: `self.ptr` is a valid, aligned allocation of `self.len`
150 // elements, zero-initialised at `alloc` time, and we have `&mut self`
151 // so no other alias exists.
152 unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
153 }
154}
155
156impl<T: Copy> Deref for PinnedBuffer<T> {
157 type Target = [T];
158
159 #[inline]
160 fn deref(&self) -> &[T] {
161 self.as_slice()
162 }
163}
164
165impl<T: Copy> DerefMut for PinnedBuffer<T> {
166 #[inline]
167 fn deref_mut(&mut self) -> &mut [T] {
168 self.as_mut_slice()
169 }
170}
171
172impl<T: Copy> Drop for PinnedBuffer<T> {
173 fn drop(&mut self) {
174 if let Ok(api) = try_driver() {
175 // SAFETY: `self.ptr` was allocated by `cu_mem_alloc_host_v2` and
176 // has not yet been freed.
177 let rc = unsafe { (api.cu_mem_free_host)(self.ptr.cast::<c_void>()) };
178 if rc != 0 {
179 tracing::warn!(
180 cuda_error = rc,
181 len = self.len,
182 "cuMemFreeHost failed during PinnedBuffer drop"
183 );
184 }
185 }
186 }
187}
188
189// ---------------------------------------------------------------------------
190// Tests
191// ---------------------------------------------------------------------------
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn alloc_signature_compiles() {
199 let _: fn(usize) -> CudaResult<PinnedBuffer<f32>> = PinnedBuffer::alloc;
200 }
201
202 #[cfg(feature = "gpu-tests")]
203 mod gpu_tests {
204 use super::*;
205
206 /// Establishes a real CUDA context on device 0 so driver calls that
207 /// require a current context (e.g. `cuMemAllocHost_v2`) succeed.
208 /// Returns `None` if no driver/GPU is available, so tests can skip
209 /// gracefully.
210 fn real_context() -> Option<oxicuda_driver::context::Context> {
211 if oxicuda_driver::init().is_err()
212 || oxicuda_driver::device::Device::count().unwrap_or(0) == 0
213 {
214 return None;
215 }
216 let dev = oxicuda_driver::device::Device::get(0).ok()?;
217 oxicuda_driver::context::Context::new(&dev).ok()
218 }
219
220 /// Regression test for F070: a freshly allocated `PinnedBuffer` must
221 /// never expose driver-uninitialised bytes through the safe
222 /// `as_slice`/`Deref` accessors — it must read back as all-zero.
223 #[test]
224 fn alloc_is_zero_initialized() {
225 let Some(_ctx) = real_context() else {
226 eprintln!("skipping: no CUDA driver/device");
227 return;
228 };
229 let Ok(buf) = PinnedBuffer::<u8>::alloc(4096) else {
230 eprintln!("skipping: alloc failed");
231 return;
232 };
233 assert_eq!(buf.len(), 4096);
234 assert!(buf.as_slice().iter().all(|&b| b == 0));
235 }
236
237 #[test]
238 fn from_slice_round_trips() {
239 let Some(_ctx) = real_context() else {
240 eprintln!("skipping: no CUDA driver/device");
241 return;
242 };
243 let data: Vec<f32> = (0..64).map(|i| i as f32).collect();
244 let Ok(buf) = PinnedBuffer::from_slice(&data) else {
245 eprintln!("skipping: alloc failed");
246 return;
247 };
248 assert_eq!(&*buf, data.as_slice());
249 }
250 }
251}