Skip to main content

j2k_cuda_runtime/memory/
pool.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3mod cache_policy;
4mod readback;
5mod reuse_guard;
6mod size_buckets;
7
8use self::cache_policy::{
9    checked_deferred_bytes, observe_deferred_high_water, CudaBufferPoolMetrics,
10};
11pub use self::cache_policy::{CudaBufferPoolDiagnostics, CudaBufferPoolLimits};
12#[cfg(test)]
13pub(crate) use self::readback::copy_pooled_bytes_to_vec_uninit;
14pub(crate) use self::readback::copy_pooled_bytes_to_vec_uninit_with_budget;
15pub(crate) use self::reuse_guard::CudaBufferPoolReuseGuard;
16use self::size_buckets::CudaBufferPoolSizeBuckets;
17use super::{pinned_staging::select_pinned_upload_result, CudaDeviceBuffer};
18use crate::{
19    allocation::host_allocation_error,
20    bytes::{f32_slice_as_bytes, i16_slice_as_bytes},
21    context::CudaContext,
22    error::CudaError,
23};
24use std::{
25    ffi::c_void,
26    sync::{Arc, Mutex},
27};
28
29/// Reusable CUDA device-buffer pool for repeated adapter dispatches.
30#[derive(Clone, Debug)]
31pub struct CudaBufferPool {
32    pub(crate) inner: Arc<CudaBufferPoolInner>,
33}
34
35#[derive(Debug)]
36pub(crate) struct CudaBufferPoolInner {
37    pub(crate) context: CudaContext,
38    pub(crate) limits: CudaBufferPoolLimits,
39    pub(crate) state: Mutex<CudaBufferPoolState>,
40}
41
42#[derive(Debug)]
43pub(crate) struct CudaBufferPoolState {
44    pub(crate) free: CudaBufferPoolFree,
45    pub(crate) deferred: Vec<CudaDeviceBuffer>,
46    pub(crate) deferred_bytes: usize,
47    pub(crate) reuse_holds: usize,
48    metrics: CudaBufferPoolMetrics,
49}
50
51#[derive(Debug)]
52pub(crate) enum CudaBufferPoolFree {
53    FirstFit(Vec<CudaDeviceBuffer>),
54    SizeBuckets(CudaBufferPoolSizeBuckets),
55}
56
57impl CudaBufferPoolInner {
58    fn recycle_buffer(&self, buffer: CudaDeviceBuffer) -> Result<(), CudaError> {
59        if !buffer.is_owned_by(&self.context) {
60            return Err(CudaError::InvalidArgument {
61                message: "CUDA buffer must belong to the pool's context".to_string(),
62            });
63        }
64        if let Err(error) = self.context.inner.ensure_resource_lifetime_available() {
65            drop(buffer);
66            return Err(error);
67        }
68        let mut state = match self.state.lock() {
69            Ok(state) => state,
70            Err(error) => {
71                // Pool invariants are unknown after poisoning. Retain the
72                // allocation instead of letting it be freed while queued CUDA
73                // work may still reference it.
74                std::mem::forget(buffer);
75                return Err(CudaError::StatePoisoned {
76                    message: error.to_string(),
77                });
78            }
79        };
80        if state.reuse_holds != 0 {
81            let deferred_bytes =
82                match checked_deferred_bytes(state.deferred_bytes, buffer.byte_len()) {
83                    Ok(bytes) => bytes,
84                    Err(error) => {
85                        // Queued work may still reference the allocation. Preserve
86                        // the token when even the safety ledger cannot represent it.
87                        std::mem::forget(buffer);
88                        return Err(error);
89                    }
90                };
91            if state.deferred.try_reserve(1).is_err() {
92                let error = host_allocation_error::<CudaDeviceBuffer>(
93                    state.deferred.len().saturating_add(1),
94                );
95                // Queued CUDA work may still reference this allocation. If
96                // retention metadata cannot grow, intentionally leak the
97                // device token instead of running its destructor early.
98                std::mem::forget(buffer);
99                return Err(error);
100            }
101            state.deferred.push(buffer);
102            state.deferred_bytes = deferred_bytes;
103            observe_deferred_high_water(&mut state);
104            return Ok(());
105        }
106        drop(state);
107        self.recycle_completed_buffer(buffer)
108    }
109
110    fn release_reuse_hold(&self) -> Result<(), CudaError> {
111        let deferred = {
112            let mut state = self
113                .state
114                .lock()
115                .map_err(|error| CudaError::StatePoisoned {
116                    message: error.to_string(),
117                })?;
118            release_reuse_hold_state(&mut state)?
119        };
120        if let Some(deferred) = deferred {
121            // Completion is established before the final hold is released.
122            // Admission and device release both happen outside the state lock.
123            for buffer in deferred {
124                self.recycle_completed_buffer(buffer)?;
125            }
126        }
127        Ok(())
128    }
129}
130
131fn acquire_reuse_hold(state: &mut CudaBufferPoolState) -> Result<(), CudaError> {
132    state.reuse_holds =
133        state
134            .reuse_holds
135            .checked_add(1)
136            .ok_or_else(|| CudaError::InvalidArgument {
137                message: "CUDA buffer pool reuse hold count overflow".to_string(),
138            })?;
139    Ok(())
140}
141
142fn release_reuse_hold_state(
143    state: &mut CudaBufferPoolState,
144) -> Result<Option<Vec<CudaDeviceBuffer>>, CudaError> {
145    if state.reuse_holds == 0 {
146        return Err(CudaError::InvalidArgument {
147            message: "CUDA buffer pool reuse hold is already released".to_string(),
148        });
149    }
150    state.reuse_holds -= 1;
151    if state.reuse_holds == 0 {
152        let deferred = std::mem::take(&mut state.deferred);
153        state.deferred_bytes = 0;
154        return Ok(Some(deferred));
155    }
156    Ok(None)
157}
158
159#[cfg(test)]
160mod reuse_hold_tests {
161    use super::*;
162
163    #[test]
164    fn nested_pool_reuse_holds_release_only_at_zero() {
165        let mut state = CudaBufferPoolState {
166            free: CudaBufferPoolFree::FirstFit(Vec::new()),
167            deferred: Vec::new(),
168            deferred_bytes: 0,
169            reuse_holds: 0,
170            metrics: CudaBufferPoolMetrics::default(),
171        };
172
173        acquire_reuse_hold(&mut state).expect("first reuse hold");
174        acquire_reuse_hold(&mut state).expect("nested reuse hold");
175        assert_eq!(state.reuse_holds, 2);
176
177        assert!(release_reuse_hold_state(&mut state)
178            .expect("release nested hold")
179            .is_none());
180        assert_eq!(state.reuse_holds, 1);
181        assert!(release_reuse_hold_state(&mut state)
182            .expect("release final hold")
183            .is_some());
184        assert_eq!(state.reuse_holds, 0);
185        assert_eq!(state.deferred_bytes, 0);
186        assert!(matches!(
187            release_reuse_hold_state(&mut state),
188            Err(CudaError::InvalidArgument { .. })
189        ));
190    }
191}
192
193#[doc(hidden)]
194/// Diagnostics for one traced [`CudaBufferPool`] acquisition.
195#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
196pub struct CudaBufferPoolTakeTrace {
197    /// Requested byte length for the checkout.
198    pub requested_len: usize,
199    /// Number of cached free buffers before the checkout.
200    pub free_count_before: usize,
201    /// Number of cached entries examined while finding a reusable buffer or allocating.
202    pub scanned_count: usize,
203    /// Whether the checkout reused a cached allocation.
204    pub reused: bool,
205    /// Actual allocation byte length backing the checkout.
206    pub allocation_byte_len: usize,
207}
208
209impl CudaBufferPool {
210    /// Acquire a device buffer with at least `len` bytes.
211    pub fn take(&self, len: usize) -> Result<CudaPooledDeviceBuffer, CudaError> {
212        self.inner
213            .context
214            .inner
215            .ensure_resource_lifetime_available()?;
216        let mut state = self
217            .inner
218            .state
219            .lock()
220            .map_err(|error| CudaError::StatePoisoned {
221                message: error.to_string(),
222            })?;
223        let (reusable_buffer, _) = pool_take_fit_buffer(&mut state.free, len);
224        let buffer = if let Some(buffer) = reusable_buffer {
225            buffer
226        } else {
227            drop(state);
228            self.inner.context.allocate(len)?
229        };
230        Ok(CudaPooledDeviceBuffer {
231            buffer: Some(buffer),
232            requested_len: len,
233            pool: self.inner.clone(),
234        })
235    }
236
237    /// Return a raw device buffer to this pool.
238    pub fn recycle(&self, buffer: CudaDeviceBuffer) -> Result<(), CudaError> {
239        self.inner.recycle_buffer(buffer)
240    }
241
242    #[doc(hidden)]
243    /// Acquire a device buffer with diagnostics for profiling pool behavior.
244    pub fn take_with_trace(
245        &self,
246        len: usize,
247    ) -> Result<(CudaPooledDeviceBuffer, CudaBufferPoolTakeTrace), CudaError> {
248        self.inner
249            .context
250            .inner
251            .ensure_resource_lifetime_available()?;
252        let mut state = self
253            .inner
254            .state
255            .lock()
256            .map_err(|error| CudaError::StatePoisoned {
257                message: error.to_string(),
258            })?;
259        let free_count_before = state.free.cached_count();
260        let (reusable_buffer, scanned_count) = pool_take_fit_buffer(&mut state.free, len);
261        let reused = reusable_buffer.is_some();
262        let buffer = if let Some(buffer) = reusable_buffer {
263            buffer
264        } else {
265            drop(state);
266            self.inner.context.allocate(len)?
267        };
268        let allocation_byte_len = buffer.byte_len();
269        let trace = CudaBufferPoolTakeTrace {
270            requested_len: len,
271            free_count_before,
272            scanned_count,
273            reused,
274            allocation_byte_len,
275        };
276        Ok((
277            CudaPooledDeviceBuffer {
278                buffer: Some(buffer),
279                requested_len: len,
280                pool: self.inner.clone(),
281            },
282            trace,
283        ))
284    }
285
286    /// Upload host bytes into a pooled device buffer.
287    pub fn upload(&self, bytes: &[u8]) -> Result<CudaPooledDeviceBuffer, CudaError> {
288        let buffer = self.take(bytes.len())?;
289        if !bytes.is_empty() {
290            self.inner
291                .context
292                .inner
293                .with_current_resource_operation(|| {
294                    // SAFETY: `buffer` is a live device allocation with at
295                    // least `bytes.len()` bytes, and `bytes` covers that many
296                    // host bytes while the lifecycle gate is held.
297                    let result = unsafe {
298                        (self.inner.context.inner.driver.cu_memcpy_htod)(
299                            buffer.device_ptr(),
300                            bytes.as_ptr().cast::<c_void>(),
301                            bytes.len(),
302                        )
303                    };
304                    self.inner
305                        .context
306                        .inner
307                        .driver
308                        .check("cuMemcpyHtoD_v2", result)
309                })?;
310            self.inner.context.record_host_to_device_copy(bytes.len());
311        }
312        Ok(buffer)
313    }
314
315    /// Upload host bytes through temporary page-locked staging into a pooled device buffer.
316    pub fn upload_pinned(&self, bytes: &[u8]) -> Result<CudaPooledDeviceBuffer, CudaError> {
317        if bytes.is_empty() {
318            return self.upload(bytes);
319        }
320
321        let operation = self.inner.context.begin_pinned_upload_operation()?;
322        let buffer = self.take(bytes.len())?;
323        let mut staging = operation.prepare_upload(bytes.len())?;
324        staging.copy_from_slice(bytes)?;
325        let staging_bytes = staging.as_slice()?;
326        let upload_result = self
327            .inner
328            .context
329            .inner
330            .with_current_resource_operation(|| {
331                // SAFETY: `buffer` is a live device allocation with at least
332                // `bytes.len()` bytes, the pinned staging slice covers that
333                // range, and the lifecycle gate is held.
334                let result = unsafe {
335                    (self.inner.context.inner.driver.cu_memcpy_htod)(
336                        buffer.device_ptr(),
337                        staging_bytes.as_ptr().cast::<c_void>(),
338                        bytes.len(),
339                    )
340                };
341                self.inner
342                    .context
343                    .inner
344                    .driver
345                    .check("cuMemcpyHtoD_v2", result)
346            });
347        if upload_result.is_ok() {
348            self.inner.context.record_host_to_device_copy(bytes.len());
349        }
350        let recycle_result = staging.recycle();
351        select_pinned_upload_result(upload_result.map(|()| buffer), recycle_result)
352    }
353
354    /// Upload host `f32` samples into a pooled device buffer.
355    pub fn upload_f32(&self, samples: &[f32]) -> Result<CudaPooledDeviceBuffer, CudaError> {
356        self.upload(f32_slice_as_bytes(samples))
357    }
358
359    /// Upload host `f32` samples through pinned staging into a pooled device buffer.
360    pub fn upload_f32_pinned(&self, samples: &[f32]) -> Result<CudaPooledDeviceBuffer, CudaError> {
361        self.upload_pinned(f32_slice_as_bytes(samples))
362    }
363
364    #[doc(hidden)]
365    /// Upload host `i16` samples into a pooled device buffer.
366    pub fn upload_i16(&self, samples: &[i16]) -> Result<CudaPooledDeviceBuffer, CudaError> {
367        self.upload(i16_slice_as_bytes(samples))
368    }
369
370    #[doc(hidden)]
371    /// Upload host `i16` samples through pinned staging into a pooled device buffer.
372    pub fn upload_i16_pinned(&self, samples: &[i16]) -> Result<CudaPooledDeviceBuffer, CudaError> {
373        self.upload_pinned(i16_slice_as_bytes(samples))
374    }
375
376    /// Number of free buffers currently cached by the pool.
377    pub fn cached_count(&self) -> Result<usize, CudaError> {
378        self.inner
379            .context
380            .inner
381            .ensure_resource_lifetime_available()?;
382        Ok(self
383            .inner
384            .state
385            .lock()
386            .map_err(|error| CudaError::StatePoisoned {
387                message: error.to_string(),
388            })?
389            .free
390            .cached_count())
391    }
392
393    pub(crate) fn defer_reuse(&self) -> Result<CudaBufferPoolReuseGuard, CudaError> {
394        self.inner
395            .context
396            .inner
397            .ensure_resource_lifetime_available()?;
398        let mut state = self
399            .inner
400            .state
401            .lock()
402            .map_err(|error| CudaError::StatePoisoned {
403                message: error.to_string(),
404            })?;
405        acquire_reuse_hold(&mut state)?;
406        drop(state);
407        Ok(CudaBufferPoolReuseGuard {
408            pool: self.inner.clone(),
409            active: true,
410        })
411    }
412
413    pub(crate) fn is_owned_by(&self, context: &CudaContext) -> bool {
414        self.inner.context.is_same_context(context)
415    }
416}
417
418impl CudaBufferPoolFree {
419    fn cached_count(&self) -> usize {
420        match self {
421            Self::FirstFit(free) => free.len(),
422            Self::SizeBuckets(free) => free.cached_count(),
423        }
424    }
425}
426
427pub(crate) fn pool_take_fit_buffer(
428    free: &mut CudaBufferPoolFree,
429    len: usize,
430) -> (Option<CudaDeviceBuffer>, usize) {
431    match free {
432        CudaBufferPoolFree::FirstFit(free) => pool_take_first_fit_buffer(free, len),
433        CudaBufferPoolFree::SizeBuckets(free) => free.take(len),
434    }
435}
436
437pub(crate) fn pool_take_first_fit_buffer(
438    free: &mut Vec<CudaDeviceBuffer>,
439    len: usize,
440) -> (Option<CudaDeviceBuffer>, usize) {
441    let mut examined = 0usize;
442    for (index, buffer) in free.iter().enumerate() {
443        examined = examined.saturating_add(1);
444        if buffer.byte_len() >= len {
445            return (Some(free.remove(index)), examined);
446        }
447    }
448    (None, examined)
449}
450
451#[cfg(test)]
452pub(crate) fn pool_fit_buffer_index_by_len<I>(lengths: I, len: usize) -> Option<usize>
453where
454    I: IntoIterator<Item = (usize, usize)>,
455{
456    let lengths = lengths.into_iter().collect::<Vec<_>>();
457    let mut left = 0usize;
458    let mut right = lengths.len();
459    while left < right {
460        let mid = left + (right - left) / 2;
461        if lengths[mid].1 < len {
462            left = mid + 1;
463        } else {
464            right = mid;
465        }
466    }
467    (left < lengths.len()).then_some(lengths[left].0)
468}
469
470/// Device buffer borrowed from a [`CudaBufferPool`].
471#[derive(Debug)]
472pub struct CudaPooledDeviceBuffer {
473    pub(crate) buffer: Option<CudaDeviceBuffer>,
474    pub(crate) requested_len: usize,
475    pub(crate) pool: Arc<CudaBufferPoolInner>,
476}
477
478impl CudaPooledDeviceBuffer {
479    /// Raw CUDA device pointer value for kernel argument binding.
480    pub fn device_ptr(&self) -> u64 {
481        self.buffer.as_ref().map_or(0, CudaDeviceBuffer::device_ptr)
482    }
483
484    /// Requested byte length for the current checkout.
485    pub fn byte_len(&self) -> usize {
486        self.requested_len
487    }
488
489    /// Actual device allocation byte length.
490    pub fn allocation_byte_len(&self) -> usize {
491        self.buffer.as_ref().map_or(0, CudaDeviceBuffer::byte_len)
492    }
493
494    /// Borrow the underlying device buffer while the checkout is live.
495    pub fn as_device_buffer(&self) -> Option<&CudaDeviceBuffer> {
496        self.buffer.as_ref()
497    }
498
499    /// Detach and return the underlying device buffer instead of recycling it
500    /// when this checkout is dropped.
501    pub fn into_device_buffer(mut self) -> Result<CudaDeviceBuffer, CudaError> {
502        self.buffer
503            .take()
504            .ok_or_else(|| CudaError::InvalidArgument {
505                message: "pooled CUDA buffer checkout is empty".to_string(),
506            })
507    }
508
509    /// Copy the requested bytes for this checkout into caller-owned host output.
510    pub fn copy_to_host(&self, out: &mut [u8]) -> Result<(), CudaError> {
511        if out.len() < self.requested_len {
512            return Err(CudaError::OutputTooSmall {
513                required: self.requested_len,
514                have: out.len(),
515            });
516        }
517        if self.requested_len == 0 {
518            return Ok(());
519        }
520        let buffer = self
521            .buffer
522            .as_ref()
523            .ok_or_else(|| CudaError::InvalidArgument {
524                message: "pooled CUDA buffer checkout is empty".to_string(),
525            })?;
526        buffer.context.inner.with_current_resource_operation(|| {
527            // SAFETY: `buffer.ptr` is a live allocation with at least
528            // `requested_len` bytes and `out` covers that range while the
529            // lifecycle gate is held.
530            let result = unsafe {
531                (buffer.context.inner.driver.cu_memcpy_dtoh)(
532                    out.as_mut_ptr().cast::<c_void>(),
533                    buffer.ptr,
534                    self.requested_len,
535                )
536            };
537            buffer.context.inner.driver.check("cuMemcpyDtoH_v2", result)
538        })?;
539        buffer
540            .context
541            .record_device_to_host_copy(self.requested_len);
542        Ok(())
543    }
544}
545
546impl Drop for CudaPooledDeviceBuffer {
547    fn drop(&mut self) {
548        if let Some(buffer) = self.buffer.take() {
549            let _ = self.pool.recycle_buffer(buffer);
550        }
551    }
552}
553
554pub(crate) fn pooled_device_buffer(
555    buffer: &CudaPooledDeviceBuffer,
556) -> Result<&CudaDeviceBuffer, CudaError> {
557    buffer
558        .as_device_buffer()
559        .ok_or_else(|| CudaError::InvalidArgument {
560            message: "pooled CUDA buffer checkout is empty".to_string(),
561        })
562}