Skip to main content

openvm_cuda_common/
d_buffer.rs

1use std::{ffi::c_void, fmt::Debug, ptr};
2
3use crate::{
4    error::{check, CudaError},
5    memory_manager::{d_free, d_malloc_on},
6    stream::{cudaStream_t, GpuDeviceCtx},
7};
8
9#[link(name = "cudart")]
10extern "C" {
11    pub fn cudaMemsetAsync(dst: *mut c_void, value: i32, count: usize, stream: cudaStream_t)
12        -> i32;
13}
14
15/// Struct that owns a buffer allocated on GPU device. The struct only holds the raw pointer and
16/// length, but this struct has a `Drop` implementation which frees the associated device memory.
17#[repr(C)]
18pub struct DeviceBuffer<T> {
19    ptr: *mut T,
20    len: usize,
21    #[cfg(feature = "debug-cuda-stream")]
22    alloc_stream: cudaStream_t,
23}
24
25/// A struct that packs a pointer with a size in bytes to pass on CUDA.
26/// It holds `*const c_void` for being a universal simple type that can be read by CUDA.
27/// Since it is hard to enforce immutability preservation, it just holds `*const`,
28/// but has two separate constructors for more robustness from the usage perspective.
29/// This is essentially a [DeviceBuffer] but without owning the data.
30#[repr(C)]
31#[derive(Debug, Copy, Clone)]
32pub struct DeviceBufferView {
33    pub ptr: *const c_void,
34    pub size: usize,
35}
36
37unsafe impl<T> Send for DeviceBuffer<T> {}
38unsafe impl<T> Sync for DeviceBuffer<T> {}
39
40impl<T> DeviceBuffer<T> {
41    /// Creates an "empty" DeviceBuffer with a null pointer and zero length.
42    #[allow(clippy::new_without_default)]
43    pub fn new() -> Self {
44        DeviceBuffer {
45            ptr: ptr::null_mut(),
46            len: 0,
47            #[cfg(feature = "debug-cuda-stream")]
48            alloc_stream: std::ptr::null_mut(),
49        }
50    }
51
52    /// # Safety
53    /// - The caller must ensure that the pointer `ptr` is valid for `len` elements of type `T` in
54    ///   device memory.
55    /// - Dropping the constructed buffer will attempt to free the memory. As such, `ptr` must
56    ///   either have been allocated by the internal memory manager (VPMM) or the caller must use
57    ///   `ManuallyDrop` to prevent double-free.
58    pub unsafe fn from_raw_parts(ptr: *mut T, len: usize) -> Self {
59        DeviceBuffer {
60            ptr,
61            len,
62            #[cfg(feature = "debug-cuda-stream")]
63            alloc_stream: std::ptr::null_mut(),
64        }
65    }
66
67    /// Allocate device memory for `len` elements of type `T` on an explicit stream.
68    pub fn with_capacity_on(len: usize, device_ctx: &GpuDeviceCtx) -> Self {
69        tracing::debug!(
70            "Creating device buffer of size {} (sizeof type = {}) on stream {:?}",
71            len,
72            size_of::<T>(),
73            device_ctx.stream
74        );
75        assert_ne!(len, 0, "Zero capacity request is wrong");
76        let size_bytes = std::mem::size_of::<T>() * len;
77        let raw_ptr = d_malloc_on(size_bytes, &device_ctx.stream).expect("GPU allocation failed");
78        #[cfg(feature = "touchemall")]
79        {
80            unsafe {
81                cudaMemsetAsync(raw_ptr, 0xff, size_bytes, device_ctx.stream.as_raw());
82            }
83        }
84        let typed_ptr = raw_ptr as *mut T;
85
86        DeviceBuffer {
87            ptr: typed_ptr,
88            len,
89            #[cfg(feature = "debug-cuda-stream")]
90            alloc_stream: device_ctx.stream.as_raw(),
91        }
92    }
93
94    /// Fills the buffer with zeros on an explicit stream.
95    ///
96    /// The caller should use the same stream that allocated this buffer.
97    /// `fill_zero` is async; same-stream guarantees ordering without explicit sync.
98    pub fn fill_zero_on(&self, device_ctx: &GpuDeviceCtx) -> Result<(), CudaError> {
99        if self.len == 0 {
100            return Ok(());
101        }
102        #[cfg(feature = "debug-cuda-stream")]
103        debug_assert_eq!(
104            device_ctx.stream.as_raw(),
105            self.alloc_stream,
106            "fill_zero_on: stream mismatch"
107        );
108        let size_bytes = std::mem::size_of::<T>() * self.len;
109        check(unsafe {
110            cudaMemsetAsync(
111                self.as_mut_raw_ptr(),
112                0,
113                size_bytes,
114                device_ctx.stream.as_raw(),
115            )
116        })
117    }
118
119    /// Fills a suffix of the buffer with zeros on an explicit stream.
120    pub fn fill_zero_suffix_on(
121        &self,
122        start_idx: usize,
123        device_ctx: &GpuDeviceCtx,
124    ) -> Result<(), CudaError> {
125        assert!(
126            start_idx <= self.len,
127            "start index has to be smaller than or equal to length"
128        );
129        if start_idx == self.len {
130            return Ok(());
131        }
132        #[cfg(feature = "debug-cuda-stream")]
133        debug_assert_eq!(
134            device_ctx.stream.as_raw(),
135            self.alloc_stream,
136            "fill_zero_suffix_on: stream mismatch"
137        );
138        let size_bytes = std::mem::size_of::<T>() * (self.len - start_idx);
139        check(unsafe {
140            cudaMemsetAsync(
141                self.as_mut_ptr().add(start_idx) as *mut c_void,
142                0,
143                size_bytes,
144                device_ctx.stream.as_raw(),
145            )
146        })
147    }
148
149    /// Returns the number of elements in this buffer.
150    pub fn len(&self) -> usize {
151        self.len
152    }
153
154    /// Returns whether the buffer is empty (null pointer or zero length).
155    pub fn is_empty(&self) -> bool {
156        self.len == 0 || self.ptr.is_null()
157    }
158
159    /// Returns a raw mutable pointer to the device data (typed).
160    pub fn as_mut_ptr(&self) -> *mut T {
161        self.ptr
162    }
163
164    /// Returns a raw const pointer to the device data (typed).
165    pub fn as_ptr(&self) -> *const T {
166        self.ptr as *const T
167    }
168
169    /// Returns a `*mut c_void` (untyped) pointer.
170    pub fn as_mut_raw_ptr(&self) -> *mut c_void {
171        self.ptr as *mut c_void
172    }
173
174    /// Returns a `*const c_void` (untyped) pointer.
175    pub fn as_raw_ptr(&self) -> *const c_void {
176        self.ptr as *const c_void
177    }
178
179    /// Converts the buffer to a buffer of different type.
180    /// `T` must be composable of `U`s.
181    pub fn as_buffer<U>(mut self) -> DeviceBuffer<U> {
182        assert_eq!(
183            size_of::<T>() % size_of::<U>(),
184            0,
185            "the underlying type size must divide the former one"
186        );
187        assert_eq!(
188            align_of::<T>() % align_of::<U>(),
189            0,
190            "the underlying type alignment must divide the former one"
191        );
192        let res = DeviceBuffer {
193            ptr: self.ptr as *mut U,
194            len: self.len * (size_of::<T>() / size_of::<U>()),
195            #[cfg(feature = "debug-cuda-stream")]
196            alloc_stream: self.alloc_stream,
197        };
198        self.ptr = ptr::null_mut(); // for safe drop
199        self.len = 0;
200        res
201    }
202
203    pub fn view(&self) -> DeviceBufferView {
204        DeviceBufferView {
205            ptr: self.ptr as *const c_void,
206            size: self.len * size_of::<T>(),
207        }
208    }
209}
210
211impl<T> Drop for DeviceBuffer<T> {
212    fn drop(&mut self) {
213        if !self.ptr.is_null() {
214            tracing::debug!(
215                "Freeing device buffer of size {} (sizeof type = {})",
216                self.len,
217                size_of::<T>()
218            );
219            // d_free enqueues cudaFreeAsync on the stream that originally
220            // allocated this buffer (stored in the memory manager's record).
221            // This is correct even when Drop runs on a different thread —
222            // CUDA handles cross-thread stream operations safely.
223            unsafe {
224                d_free(self.ptr as *mut c_void).expect("GPU free failed");
225            }
226            self.ptr = ptr::null_mut();
227            self.len = 0;
228        }
229    }
230}
231
232impl<T: Debug> Debug for DeviceBuffer<T> {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        write!(
235            f,
236            "DeviceBuffer(len = {}, ptr = {:?})",
237            self.len(),
238            self.ptr
239        )
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::copy::{MemCopyD2H, MemCopyH2D};
247
248    fn test_ctx() -> GpuDeviceCtx {
249        GpuDeviceCtx::for_current_device().unwrap()
250    }
251
252    #[test]
253    fn test_device_buffer_float() {
254        let device_ctx = test_ctx();
255        // Create a DeviceBuffer of 10 floats
256        let db = DeviceBuffer::<f32>::with_capacity_on(10, &device_ctx);
257
258        assert_eq!(db.len(), 10);
259        assert!(!db.as_ptr().is_null());
260        assert!(!db.is_empty());
261
262        // The buffer will be automatically freed at the end of this test
263    }
264
265    #[test]
266    fn test_device_buffer_fill_zero() {
267        let device_ctx = test_ctx();
268        let v: Vec<u64> = (0..10).collect();
269        let d_array = v.to_device_on(&device_ctx).unwrap();
270        d_array.fill_zero_on(&device_ctx).unwrap();
271        assert_eq!(d_array.to_host_on(&device_ctx).unwrap(), vec![0; v.len()]);
272    }
273}