oxicuda_memory/zero_copy.rs
1//! Zero-copy (host-mapped) memory.
2//!
3//! Allows GPU kernels to directly access host memory without explicit
4//! transfers. Useful for small, frequently-updated data or when PCIe
5//! bandwidth is acceptable.
6//!
7//! # How it works
8//!
9//! Zero-copy memory is allocated on the host using `cuMemAllocHost_v2`,
10//! which allocates page-locked (pinned) memory that the CUDA driver maps
11//! into the device's address space. A corresponding device pointer is
12//! obtained via `cuMemHostGetDevicePointer_v2`. GPU reads and writes
13//! traverse the PCIe bus on each access, so this is best suited for data
14//! that is accessed infrequently or streamed sequentially.
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use oxicuda_memory::zero_copy::MappedBuffer;
20//!
21//! oxicuda_driver::init()?;
22//! let _ = oxicuda_driver::primary_context::PrimaryContext::retain(
23//! &oxicuda_driver::device::Device::get(0)?
24//! )?;
25//!
26//! let mut buf = MappedBuffer::<f32>::alloc(256)?;
27//! // Write from the host.
28//! for (i, val) in buf.as_host_slice_mut().iter_mut().enumerate() {
29//! *val = i as f32;
30//! }
31//! // `buf.as_device_ptr()` can now be passed to a kernel.
32//! # Ok::<(), oxicuda_driver::error::CudaError>(())
33//! ```
34
35use std::ffi::c_void;
36use std::marker::PhantomData;
37use std::mem::size_of;
38
39use oxicuda_driver::error::CudaResult;
40use oxicuda_driver::ffi::CUdeviceptr;
41use oxicuda_driver::loader::try_driver;
42
43// ---------------------------------------------------------------------------
44// MappedBuffer<T>
45// ---------------------------------------------------------------------------
46
47/// A host-allocated, device-mapped (zero-copy) memory buffer.
48///
49/// The host memory is page-locked and accessible from both CPU code and GPU
50/// kernels. GPU accesses traverse the PCIe bus, making this suitable for
51/// small or infrequently-accessed data where the overhead of explicit
52/// transfers is not justified.
53///
54/// The buffer is freed automatically on drop via `cuMemFreeHost`.
55pub struct MappedBuffer<T: Copy> {
56 /// Host pointer to the pinned allocation.
57 host_ptr: *mut T,
58 /// Corresponding device pointer for kernel access.
59 device_ptr: CUdeviceptr,
60 /// Number of `T` elements.
61 len: usize,
62 /// Marker for the element type.
63 _phantom: PhantomData<T>,
64}
65
66// SAFETY: The page-locked host memory is not thread-local; both the host
67// and device pointers are valid for Send/Sync if T is.
68unsafe impl<T: Copy + Send> Send for MappedBuffer<T> {}
69unsafe impl<T: Copy + Sync> Sync for MappedBuffer<T> {}
70
71impl<T: Copy> MappedBuffer<T> {
72 /// Allocates a zero-copy host-mapped buffer of `n` elements.
73 ///
74 /// The allocation uses `cuMemAllocHost_v2` (page-locked pinned memory)
75 /// and retrieves the corresponding device pointer via
76 /// `cuMemHostGetDevicePointer_v2`. A CUDA context must be current on
77 /// the calling thread.
78 ///
79 /// # Errors
80 ///
81 /// Returns a CUDA driver error if allocation or mapping fails.
82 pub fn alloc(n: usize) -> CudaResult<Self> {
83 let api = try_driver()?;
84 let byte_size = n.saturating_mul(size_of::<T>());
85
86 // Allocate page-locked host memory.
87 let mut raw_ptr: *mut c_void = std::ptr::null_mut();
88 oxicuda_driver::error::check(unsafe {
89 (api.cu_mem_alloc_host_v2)(&mut raw_ptr, byte_size)
90 })?;
91 let host_ptr = raw_ptr.cast::<T>();
92
93 // Obtain the device-side pointer for this pinned region.
94 let mut device_ptr: CUdeviceptr = 0;
95 let result = oxicuda_driver::error::check(unsafe {
96 (api.cu_mem_host_get_device_pointer_v2)(&mut device_ptr, raw_ptr, 0)
97 });
98 if let Err(e) = result {
99 // Free the pinned allocation before propagating the error.
100 unsafe { (api.cu_mem_free_host)(raw_ptr) };
101 return Err(e);
102 }
103
104 // SAFETY: `raw_ptr` was just allocated by `cu_mem_alloc_host_v2` and
105 // is valid for `byte_size` bytes. Zero-initialising it here means
106 // `as_host_slice`/`as_host_slice_mut` below never expose
107 // driver-uninitialised memory as a safe `&[T]` (all-zero bytes are
108 // a valid `T` for every in-tree usage of `MappedBuffer`, e.g.
109 // `u8`/`f32`). Skipped for a zero-byte allocation (e.g. `n == 0` or
110 // a zero-sized `T`) to avoid writing through a possibly-unusual
111 // pointer for a no-op write.
112 if byte_size > 0 {
113 unsafe {
114 std::ptr::write_bytes(raw_ptr.cast::<u8>(), 0, byte_size);
115 }
116 }
117
118 Ok(Self {
119 host_ptr,
120 device_ptr,
121 len: n,
122 _phantom: PhantomData,
123 })
124 }
125
126 /// Returns the number of `T` elements in this buffer.
127 #[inline]
128 pub fn len(&self) -> usize {
129 self.len
130 }
131
132 /// Returns `true` if the buffer contains zero elements.
133 #[inline]
134 pub fn is_empty(&self) -> bool {
135 self.len == 0
136 }
137
138 /// Returns the byte size of this buffer.
139 #[inline]
140 pub fn byte_size(&self) -> usize {
141 self.len * size_of::<T>()
142 }
143
144 /// Returns the raw device pointer for use in kernel parameters.
145 #[inline]
146 pub fn as_device_ptr(&self) -> CUdeviceptr {
147 self.device_ptr
148 }
149
150 /// Returns a raw const pointer to the host-side data.
151 #[inline]
152 pub fn as_host_ptr(&self) -> *const T {
153 self.host_ptr
154 }
155
156 /// Returns a raw mutable pointer to the host-side data.
157 #[inline]
158 pub fn as_host_ptr_mut(&mut self) -> *mut T {
159 self.host_ptr
160 }
161
162 /// Returns a shared slice over the host-side data.
163 ///
164 /// # Safety
165 ///
166 /// The caller must ensure no concurrent GPU writes are in flight.
167 pub fn as_host_slice(&self) -> &[T] {
168 // SAFETY: host_ptr is valid for `len` elements allocated by
169 // cuMemAllocHost and zero-initialised at `alloc` time.
170 unsafe { std::slice::from_raw_parts(self.host_ptr, self.len) }
171 }
172
173 /// Returns a mutable slice over the host-side data.
174 ///
175 /// # Safety
176 ///
177 /// The caller must ensure no concurrent GPU reads or writes are in flight.
178 pub fn as_host_slice_mut(&mut self) -> &mut [T] {
179 // SAFETY: host_ptr is valid for `len` elements allocated by
180 // cuMemAllocHost and zero-initialised at `alloc` time.
181 unsafe { std::slice::from_raw_parts_mut(self.host_ptr, self.len) }
182 }
183}
184
185impl<T: Copy> Drop for MappedBuffer<T> {
186 fn drop(&mut self) {
187 if self.host_ptr.is_null() {
188 return;
189 }
190 if let Ok(api) = try_driver() {
191 // SAFETY: host_ptr was allocated by cuMemAllocHost_v2 and has not
192 // been freed yet (Drop is called at most once).
193 unsafe { (api.cu_mem_free_host)(self.host_ptr.cast::<c_void>()) };
194 }
195 }
196}
197
198// ---------------------------------------------------------------------------
199// Tests
200// ---------------------------------------------------------------------------
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn alloc_signature_compiles() {
208 let _: fn(usize) -> CudaResult<MappedBuffer<f32>> = MappedBuffer::alloc;
209 }
210
211 #[cfg(feature = "gpu-tests")]
212 mod gpu_tests {
213 use super::*;
214
215 /// Establishes a real CUDA context on device 0. Returns `None` if no
216 /// driver/GPU is available so tests can skip gracefully.
217 fn real_context() -> Option<oxicuda_driver::context::Context> {
218 if oxicuda_driver::init().is_err()
219 || oxicuda_driver::device::Device::count().unwrap_or(0) == 0
220 {
221 return None;
222 }
223 let dev = oxicuda_driver::device::Device::get(0).ok()?;
224 oxicuda_driver::context::Context::new(&dev).ok()
225 }
226
227 /// Regression test for F070: a freshly allocated `MappedBuffer` must
228 /// never expose driver-uninitialised bytes through the safe
229 /// `as_host_slice` accessor — it must read back as all-zero.
230 #[test]
231 fn alloc_is_zero_initialized() {
232 let Some(_ctx) = real_context() else {
233 eprintln!("skipping: no CUDA driver/device");
234 return;
235 };
236 let Ok(buf) = MappedBuffer::<u8>::alloc(4096) else {
237 eprintln!("skipping: alloc failed");
238 return;
239 };
240 assert_eq!(buf.len(), 4096);
241 assert!(buf.as_host_slice().iter().all(|&b| b == 0));
242 }
243
244 #[test]
245 fn device_ptr_is_nonzero_and_host_writes_visible() {
246 let Some(_ctx) = real_context() else {
247 eprintln!("skipping: no CUDA driver/device");
248 return;
249 };
250 let Ok(mut buf) = MappedBuffer::<f32>::alloc(64) else {
251 eprintln!("skipping: alloc failed");
252 return;
253 };
254 assert_ne!(buf.as_device_ptr(), 0);
255 for (i, v) in buf.as_host_slice_mut().iter_mut().enumerate() {
256 *v = i as f32;
257 }
258 let expected: Vec<f32> = (0..64).map(|i| i as f32).collect();
259 assert_eq!(buf.as_host_slice(), expected.as_slice());
260 }
261 }
262}