Skip to main content

j2k_cuda_runtime/
memory.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3mod pinned_staging;
4mod pool;
5mod ranges;
6
7pub(crate) use self::pinned_staging::PinnedUploadStagingPool;
8pub use self::pinned_staging::{
9    CudaPinnedUploadOperationGuard, CudaPinnedUploadStagingCheckout,
10    CudaPinnedUploadStagingPoolDiagnostics, CudaPinnedUploadStagingPoolLimits,
11};
12#[cfg(test)]
13pub(crate) use self::pool::copy_pooled_bytes_to_vec_uninit;
14#[cfg(test)]
15pub(crate) use self::pool::pool_fit_buffer_index_by_len;
16pub(crate) use self::pool::{
17    copy_pooled_bytes_to_vec_uninit_with_budget, pooled_device_buffer, CudaBufferPoolReuseGuard,
18};
19pub use self::pool::{
20    CudaBufferPool, CudaBufferPoolDiagnostics, CudaBufferPoolLimits, CudaBufferPoolTakeTrace,
21    CudaPooledDeviceBuffer,
22};
23pub(crate) use self::ranges::CheckedDeviceBufferRanges;
24
25#[cfg(test)]
26use crate::context::validate_non_null_pinned_host_allocation;
27use crate::{
28    bytes::f32_slice_as_bytes, context::CudaContext, driver::CuDevicePtr, error::CudaError,
29};
30use std::ffi::c_void;
31
32impl CudaContext {
33    /// Upload host bytes into a CUDA device buffer.
34    pub fn upload(&self, bytes: &[u8]) -> Result<CudaDeviceBuffer, CudaError> {
35        let mut ptr = 0;
36        let buffer = if bytes.is_empty() {
37            self.inner.set_current()?;
38            CudaDeviceBuffer {
39                context: self.clone(),
40                ptr,
41                len: bytes.len(),
42            }
43        } else {
44            self.inner.with_current_stateful_operation(|| {
45                // SAFETY: CUDA writes a device pointer for the requested byte
46                // size while this context's lifecycle gate is held.
47                self.inner.driver.check("cuMemAlloc_v2", unsafe {
48                    (self.inner.driver.cu_mem_alloc)(&raw mut ptr, bytes.len())
49                })?;
50                crate::context::validate_device_allocation(ptr, bytes.len())
51            })?;
52
53            CudaDeviceBuffer {
54                context: self.clone(),
55                ptr,
56                len: bytes.len(),
57            }
58        };
59
60        if !bytes.is_empty() {
61            self.inner.with_current_resource_operation(|| {
62                // SAFETY: ptr is a valid device allocation of bytes.len(), the
63                // host pointer covers that length, and the lifecycle gate is held.
64                self.inner.driver.check("cuMemcpyHtoD_v2", unsafe {
65                    (self.inner.driver.cu_memcpy_htod)(
66                        ptr,
67                        bytes.as_ptr().cast::<c_void>(),
68                        bytes.len(),
69                    )
70                })
71            })?;
72        }
73
74        Ok(buffer)
75    }
76
77    /// Upload host `f32` samples into a CUDA device buffer.
78    pub fn upload_f32(&self, samples: &[f32]) -> Result<CudaDeviceBuffer, CudaError> {
79        self.upload(f32_slice_as_bytes(samples))
80    }
81
82    /// Allocate an uninitialized CUDA device buffer.
83    pub fn allocate(&self, len: usize) -> Result<CudaDeviceBuffer, CudaError> {
84        let mut ptr = 0;
85        if len != 0 {
86            self.inner.with_current_stateful_operation(|| {
87                // SAFETY: CUDA writes a device pointer for the requested byte
88                // size while this context's lifecycle gate is held.
89                self.inner.driver.check("cuMemAlloc_v2", unsafe {
90                    (self.inner.driver.cu_mem_alloc)(&raw mut ptr, len)
91                })?;
92                crate::context::validate_device_allocation(ptr, len)
93            })?;
94        } else {
95            self.inner.set_current()?;
96        }
97        Ok(CudaDeviceBuffer {
98            context: self.clone(),
99            ptr,
100            len,
101        })
102    }
103
104    /// Allocate page-locked host memory for host-to-device staging.
105    #[cfg(test)]
106    pub(crate) fn pinned_host_buffer(&self, len: usize) -> Result<CudaPinnedHostBuffer, CudaError> {
107        let mut ptr = std::ptr::null_mut();
108        if len != 0 {
109            self.inner.with_current_stateful_operation(|| {
110                // SAFETY: CUDA writes a page-locked host pointer for the requested
111                // byte length. The allocation is freed by CudaPinnedHostBuffer.
112                self.inner.driver.check("cuMemHostAlloc", unsafe {
113                    (self.inner.driver.cu_mem_host_alloc)(&raw mut ptr, len, 0)
114                })?;
115                validate_non_null_pinned_host_allocation(ptr.cast::<u8>(), len).map(|_| ())
116            })?;
117        } else {
118            self.inner.set_current()?;
119        }
120        Ok(CudaPinnedHostBuffer {
121            context: self.clone(),
122            ptr: ptr.cast::<u8>(),
123            len,
124        })
125    }
126
127    /// Create a reusable device-buffer pool for this context.
128    pub fn buffer_pool(&self) -> CudaBufferPool {
129        CudaBufferPool::new(self.clone())
130    }
131
132    /// Create a reusable best-fit device-buffer pool for workloads with many
133    /// same-sized intermediate buffers.
134    pub fn best_fit_buffer_pool(&self) -> CudaBufferPool {
135        CudaBufferPool::new_size_buckets(self.clone())
136    }
137}
138
139/// Page-locked host staging buffer.
140#[cfg(test)]
141#[derive(Debug)]
142pub(crate) struct CudaPinnedHostBuffer {
143    pub(crate) context: CudaContext,
144    pub(crate) ptr: *mut u8,
145    pub(crate) len: usize,
146}
147
148#[cfg(test)]
149impl CudaPinnedHostBuffer {
150    /// Immutable byte view of the pinned allocation.
151    pub(crate) fn as_slice(&self) -> &[u8] {
152        if self.len == 0 {
153            &[]
154        } else {
155            // SAFETY: ptr is a live pinned allocation of len bytes.
156            unsafe { std::slice::from_raw_parts(self.ptr.cast_const(), self.len) }
157        }
158    }
159
160    /// Mutable byte view of the pinned allocation.
161    pub(crate) fn as_mut_slice(&mut self) -> &mut [u8] {
162        if self.len == 0 {
163            &mut []
164        } else {
165            // SAFETY: ptr is uniquely borrowed through &mut self and covers len
166            // bytes allocated by CUDA.
167            unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
168        }
169    }
170}
171
172#[cfg(test)]
173impl Drop for CudaPinnedHostBuffer {
174    fn drop(&mut self) {
175        if !self.ptr.is_null() {
176            let free_result = self.context.inner.with_current_stateful_operation(|| {
177                // SAFETY: ptr was returned by cuMemHostAlloc for this process,
178                // and the context lifecycle gate is held during destruction.
179                self.context.inner.driver.check("cuMemFreeHost", unsafe {
180                    (self.context.inner.driver.cu_mem_free_host)(self.ptr.cast())
181                })
182            });
183            if free_result.is_err() {
184                std::mem::forget(self.context.clone());
185            }
186        }
187    }
188}
189
190// SAFETY: The pinned allocation is owned by this value and CUDA frees it on
191// drop. Mutable access still requires &mut self.
192#[cfg(test)]
193unsafe impl Send for CudaPinnedHostBuffer {}
194
195/// Owned CUDA device buffer.
196#[derive(Debug)]
197pub struct CudaDeviceBuffer {
198    pub(crate) context: CudaContext,
199    pub(crate) ptr: CuDevicePtr,
200    pub(crate) len: usize,
201}
202
203#[doc(hidden)]
204/// Typed immutable device buffer view.
205#[derive(Clone, Copy, Debug)]
206pub struct CudaDeviceBufferView<'a, T> {
207    pub(crate) ptr: CuDevicePtr,
208    pub(crate) len: usize,
209    pub(crate) _marker: std::marker::PhantomData<&'a T>,
210}
211
212impl<T> CudaDeviceBufferView<'_, T> {
213    /// Raw CUDA device pointer value for kernel argument binding.
214    pub fn device_ptr(&self) -> u64 {
215        self.ptr
216    }
217
218    /// Number of typed elements in this view.
219    pub fn len(&self) -> usize {
220        self.len
221    }
222
223    /// Whether this view has no elements.
224    pub fn is_empty(&self) -> bool {
225        self.len == 0
226    }
227}
228
229#[doc(hidden)]
230/// Typed mutable device buffer view.
231#[derive(Debug)]
232pub struct CudaDeviceBufferViewMut<'a, T> {
233    pub(crate) ptr: CuDevicePtr,
234    pub(crate) len: usize,
235    pub(crate) _marker: std::marker::PhantomData<&'a mut T>,
236}
237
238/// Lifetime-bound mutable view of CUDA memory owned by another runtime.
239///
240/// This value never frees the allocation. Its lifetime is tied to an exclusive
241/// borrow of the external runtime's managed-resource guard.
242#[doc(hidden)]
243#[derive(Debug)]
244pub struct CudaExternalDeviceBufferViewMut<'a> {
245    context: CudaContext,
246    ptr: CuDevicePtr,
247    len: usize,
248    _exclusive: std::marker::PhantomData<&'a mut ()>,
249}
250
251impl<'a> CudaExternalDeviceBufferViewMut<'a> {
252    /// Construct a non-owning external device-buffer view.
253    ///
254    /// # Safety
255    ///
256    /// `ptr..ptr+len` must be a live CUDA allocation range represented by
257    /// `_managed_owner`. The exclusive owner borrow must exclude every
258    /// overlapping host or device mutation for this view's lifetime. The
259    /// allocation must remain valid and must not be freed by the caller until
260    /// the view is dropped. Any stream-ordered allocation operation must have
261    /// completed, or have been ordered before j2k's default-stream access,
262    /// before this constructor is called.
263    pub unsafe fn from_raw_parts<Owner>(
264        context: &CudaContext,
265        ptr: u64,
266        len: usize,
267        required_alignment: usize,
268        _managed_owner: &'a mut Owner,
269    ) -> Result<Self, CudaError> {
270        if len == 0 {
271            return Err(CudaError::InvalidArgument {
272                message: "external CUDA buffer must not be empty".to_string(),
273            });
274        }
275        if ptr == 0 {
276            return Err(CudaError::InvalidArgument {
277                message: "external CUDA buffer pointer must not be null".to_string(),
278            });
279        }
280        if required_alignment == 0 || !required_alignment.is_power_of_two() {
281            return Err(CudaError::InvalidArgument {
282                message: "external CUDA buffer alignment must be a nonzero power of two"
283                    .to_string(),
284            });
285        }
286        let len_u64 = u64::try_from(len).map_err(|_| CudaError::LengthTooLarge { len })?;
287        ptr.checked_add(len_u64)
288            .ok_or(CudaError::LengthTooLarge { len })?;
289        let ptr = context.inner.resolve_pointer_for_context(ptr)?;
290        if !ptr.is_multiple_of(required_alignment as u64) {
291            return Err(CudaError::InvalidArgument {
292                message: format!(
293                    "external CUDA buffer pointer {ptr:#x} is not aligned to {required_alignment} bytes"
294                ),
295            });
296        }
297        ptr.checked_add(len_u64)
298            .ok_or(CudaError::LengthTooLarge { len })?;
299        Ok(Self {
300            context: context.clone(),
301            ptr,
302            len,
303            _exclusive: std::marker::PhantomData,
304        })
305    }
306
307    /// Context that owns the external allocation.
308    pub fn context(&self) -> &CudaContext {
309        &self.context
310    }
311
312    /// Raw device pointer.
313    pub fn device_ptr(&self) -> u64 {
314        self.ptr
315    }
316
317    /// External allocation range length in bytes.
318    pub fn byte_len(&self) -> usize {
319        self.len
320    }
321}
322
323impl<T> CudaDeviceBufferViewMut<'_, T> {
324    /// Raw CUDA device pointer value for kernel argument binding.
325    pub fn device_ptr(&self) -> u64 {
326        self.ptr
327    }
328
329    /// Number of typed elements in this view.
330    pub fn len(&self) -> usize {
331        self.len
332    }
333
334    /// Whether this view has no elements.
335    pub fn is_empty(&self) -> bool {
336        self.len == 0
337    }
338}
339
340#[doc(hidden)]
341/// One byte range inside a contiguous CUDA batch output allocation.
342#[derive(Clone, Copy, Debug, Eq, PartialEq)]
343pub struct CudaDeviceBufferRange {
344    /// Byte offset from the start of the contiguous allocation.
345    pub offset: usize,
346    /// Byte length for this output item.
347    pub len: usize,
348}
349
350impl CudaDeviceBuffer {
351    pub(crate) fn is_owned_by(&self, context: &CudaContext) -> bool {
352        self.context.is_same_context(context)
353    }
354
355    /// CUDA context that owns this allocation.
356    pub fn context(&self) -> CudaContext {
357        self.context.clone()
358    }
359
360    /// Raw CUDA device pointer value.
361    pub fn device_ptr(&self) -> u64 {
362        self.ptr
363    }
364
365    /// Device allocation length in bytes.
366    pub fn byte_len(&self) -> usize {
367        self.len
368    }
369
370    #[doc(hidden)]
371    /// Borrow this allocation as a typed immutable device view.
372    pub fn typed_view<T>(&self) -> Result<CudaDeviceBufferView<'_, T>, CudaError> {
373        let element_size = std::mem::size_of::<T>();
374        if element_size == 0 || !self.len.is_multiple_of(element_size) {
375            return Err(CudaError::LengthNotElementAligned {
376                bytes: self.len,
377                element_size,
378            });
379        }
380        Ok(CudaDeviceBufferView {
381            ptr: self.ptr,
382            len: self.len / element_size,
383            _marker: std::marker::PhantomData,
384        })
385    }
386
387    #[doc(hidden)]
388    /// Borrow this allocation as a typed mutable device view.
389    pub fn typed_view_mut<T>(&mut self) -> Result<CudaDeviceBufferViewMut<'_, T>, CudaError> {
390        let element_size = std::mem::size_of::<T>();
391        if element_size == 0 || !self.len.is_multiple_of(element_size) {
392            return Err(CudaError::LengthNotElementAligned {
393                bytes: self.len,
394                element_size,
395            });
396        }
397        Ok(CudaDeviceBufferViewMut {
398            ptr: self.ptr,
399            len: self.len / element_size,
400            _marker: std::marker::PhantomData,
401        })
402    }
403
404    /// Copy device bytes into caller-owned host output.
405    pub fn copy_to_host(&self, out: &mut [u8]) -> Result<(), CudaError> {
406        if out.len() < self.len {
407            return Err(CudaError::OutputTooSmall {
408                required: self.len,
409                have: out.len(),
410            });
411        }
412        if self.len == 0 {
413            return Ok(());
414        }
415
416        self.context.inner.with_current_resource_operation(|| {
417            // SAFETY: ptr is a live device allocation of self.len bytes, out
418            // covers that range, and the context lifecycle gate is held.
419            self.context.inner.driver.check("cuMemcpyDtoH_v2", unsafe {
420                (self.context.inner.driver.cu_memcpy_dtoh)(
421                    out.as_mut_ptr().cast::<c_void>(),
422                    self.ptr,
423                    self.len,
424                )
425            })
426        })
427    }
428
429    /// Copy a byte range from this device buffer into caller-owned host output.
430    pub fn copy_range_to_host(&self, offset: usize, out: &mut [u8]) -> Result<(), CudaError> {
431        self.copy_byte_range_to_host_elements(offset, out)
432    }
433
434    /// Copy a byte range from this device buffer into uninitialized host output.
435    pub fn copy_range_to_host_uninit(
436        &self,
437        offset: usize,
438        out: &mut [std::mem::MaybeUninit<u8>],
439    ) -> Result<(), CudaError> {
440        self.copy_byte_range_to_host_elements(offset, out)
441    }
442
443    fn copy_byte_range_to_host_elements<T>(
444        &self,
445        offset: usize,
446        out: &mut [T],
447    ) -> Result<(), CudaError> {
448        let byte_len = out
449            .len()
450            .checked_mul(std::mem::size_of::<T>())
451            .ok_or(CudaError::LengthTooLarge { len: usize::MAX })?;
452        let end = offset
453            .checked_add(byte_len)
454            .ok_or(CudaError::LengthTooLarge { len: usize::MAX })?;
455        if end > self.len {
456            return Err(CudaError::OutputTooSmall {
457                required: end,
458                have: self.len,
459            });
460        }
461        if byte_len == 0 {
462            return Ok(());
463        }
464
465        let source = self
466            .ptr
467            .checked_add(
468                u64::try_from(offset).map_err(|_| CudaError::LengthTooLarge { len: offset })?,
469            )
470            .ok_or(CudaError::LengthTooLarge { len: usize::MAX })?;
471        self.context.inner.with_current_resource_operation(|| {
472            // SAFETY: `source` is inside this live device allocation, `out`
473            // covers exactly `byte_len` bytes, and the lifecycle gate is held.
474            self.context.inner.driver.check("cuMemcpyDtoH_v2", unsafe {
475                (self.context.inner.driver.cu_memcpy_dtoh)(
476                    out.as_mut_ptr().cast::<c_void>(),
477                    source,
478                    byte_len,
479                )
480            })
481        })
482    }
483}
484
485impl Drop for CudaDeviceBuffer {
486    fn drop(&mut self) {
487        if self.ptr != 0 {
488            let free_result = self.context.inner.with_current_stateful_operation(|| {
489                // SAFETY: ptr was allocated by this CUDA context. The context
490                // lifetime gate is held while the allocation is destroyed.
491                let status = unsafe { (self.context.inner.driver.cu_mem_free)(self.ptr) };
492                self.context.inner.driver.check("cuMemFree_v2", status)
493            });
494            if free_result.is_err() {
495                // Retain the context so neither this allocation nor any
496                // potentially in-flight work is torn down after completion
497                // became uncertain.
498                std::mem::forget(self.context.clone());
499            }
500        }
501    }
502}
503
504pub(crate) fn checked_image_words(
505    width: u32,
506    height: u32,
507    channels: usize,
508) -> Result<usize, CudaError> {
509    width
510        .try_into()
511        .ok()
512        .and_then(|width: usize| width.checked_mul(height as usize))
513        .and_then(|pixels| pixels.checked_mul(channels))
514        .ok_or(CudaError::ImageTooLarge {
515            width,
516            height,
517            channels,
518        })
519}