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