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        }
311        Ok(buffer)
312    }
313
314    /// Upload host bytes through temporary page-locked staging into a pooled device buffer.
315    pub fn upload_pinned(&self, bytes: &[u8]) -> Result<CudaPooledDeviceBuffer, CudaError> {
316        if bytes.is_empty() {
317            return self.upload(bytes);
318        }
319
320        let operation = self.inner.context.begin_pinned_upload_operation()?;
321        let buffer = self.take(bytes.len())?;
322        let mut staging = operation.prepare_upload(bytes.len())?;
323        staging.copy_from_slice(bytes)?;
324        let staging_bytes = staging.as_slice()?;
325        let upload_result = self
326            .inner
327            .context
328            .inner
329            .with_current_resource_operation(|| {
330                // SAFETY: `buffer` is a live device allocation with at least
331                // `bytes.len()` bytes, the pinned staging slice covers that
332                // range, and the lifecycle gate is held.
333                let result = unsafe {
334                    (self.inner.context.inner.driver.cu_memcpy_htod)(
335                        buffer.device_ptr(),
336                        staging_bytes.as_ptr().cast::<c_void>(),
337                        bytes.len(),
338                    )
339                };
340                self.inner
341                    .context
342                    .inner
343                    .driver
344                    .check("cuMemcpyHtoD_v2", result)
345            });
346        let recycle_result = staging.recycle();
347        select_pinned_upload_result(upload_result.map(|()| buffer), recycle_result)
348    }
349
350    /// Upload host `f32` samples into a pooled device buffer.
351    pub fn upload_f32(&self, samples: &[f32]) -> Result<CudaPooledDeviceBuffer, CudaError> {
352        self.upload(f32_slice_as_bytes(samples))
353    }
354
355    /// Upload host `f32` samples through pinned staging into a pooled device buffer.
356    pub fn upload_f32_pinned(&self, samples: &[f32]) -> Result<CudaPooledDeviceBuffer, CudaError> {
357        self.upload_pinned(f32_slice_as_bytes(samples))
358    }
359
360    #[doc(hidden)]
361    /// Upload host `i16` samples into a pooled device buffer.
362    pub fn upload_i16(&self, samples: &[i16]) -> Result<CudaPooledDeviceBuffer, CudaError> {
363        self.upload(i16_slice_as_bytes(samples))
364    }
365
366    #[doc(hidden)]
367    /// Upload host `i16` samples through pinned staging into a pooled device buffer.
368    pub fn upload_i16_pinned(&self, samples: &[i16]) -> Result<CudaPooledDeviceBuffer, CudaError> {
369        self.upload_pinned(i16_slice_as_bytes(samples))
370    }
371
372    /// Number of free buffers currently cached by the pool.
373    pub fn cached_count(&self) -> Result<usize, CudaError> {
374        self.inner
375            .context
376            .inner
377            .ensure_resource_lifetime_available()?;
378        Ok(self
379            .inner
380            .state
381            .lock()
382            .map_err(|error| CudaError::StatePoisoned {
383                message: error.to_string(),
384            })?
385            .free
386            .cached_count())
387    }
388
389    pub(crate) fn defer_reuse(&self) -> Result<CudaBufferPoolReuseGuard, CudaError> {
390        self.inner
391            .context
392            .inner
393            .ensure_resource_lifetime_available()?;
394        let mut state = self
395            .inner
396            .state
397            .lock()
398            .map_err(|error| CudaError::StatePoisoned {
399                message: error.to_string(),
400            })?;
401        acquire_reuse_hold(&mut state)?;
402        drop(state);
403        Ok(CudaBufferPoolReuseGuard {
404            pool: self.inner.clone(),
405            active: true,
406        })
407    }
408
409    pub(crate) fn is_owned_by(&self, context: &CudaContext) -> bool {
410        self.inner.context.is_same_context(context)
411    }
412}
413
414impl CudaBufferPoolFree {
415    fn cached_count(&self) -> usize {
416        match self {
417            Self::FirstFit(free) => free.len(),
418            Self::SizeBuckets(free) => free.cached_count(),
419        }
420    }
421}
422
423pub(crate) fn pool_take_fit_buffer(
424    free: &mut CudaBufferPoolFree,
425    len: usize,
426) -> (Option<CudaDeviceBuffer>, usize) {
427    match free {
428        CudaBufferPoolFree::FirstFit(free) => pool_take_first_fit_buffer(free, len),
429        CudaBufferPoolFree::SizeBuckets(free) => free.take(len),
430    }
431}
432
433pub(crate) fn pool_take_first_fit_buffer(
434    free: &mut Vec<CudaDeviceBuffer>,
435    len: usize,
436) -> (Option<CudaDeviceBuffer>, usize) {
437    let mut examined = 0usize;
438    for (index, buffer) in free.iter().enumerate() {
439        examined = examined.saturating_add(1);
440        if buffer.byte_len() >= len {
441            return (Some(free.remove(index)), examined);
442        }
443    }
444    (None, examined)
445}
446
447#[cfg(test)]
448pub(crate) fn pool_fit_buffer_index_by_len<I>(lengths: I, len: usize) -> Option<usize>
449where
450    I: IntoIterator<Item = (usize, usize)>,
451{
452    let lengths = lengths.into_iter().collect::<Vec<_>>();
453    let mut left = 0usize;
454    let mut right = lengths.len();
455    while left < right {
456        let mid = left + (right - left) / 2;
457        if lengths[mid].1 < len {
458            left = mid + 1;
459        } else {
460            right = mid;
461        }
462    }
463    (left < lengths.len()).then_some(lengths[left].0)
464}
465
466/// Device buffer borrowed from a [`CudaBufferPool`].
467#[derive(Debug)]
468pub struct CudaPooledDeviceBuffer {
469    pub(crate) buffer: Option<CudaDeviceBuffer>,
470    pub(crate) requested_len: usize,
471    pub(crate) pool: Arc<CudaBufferPoolInner>,
472}
473
474impl CudaPooledDeviceBuffer {
475    /// Raw CUDA device pointer value for kernel argument binding.
476    pub fn device_ptr(&self) -> u64 {
477        self.buffer.as_ref().map_or(0, CudaDeviceBuffer::device_ptr)
478    }
479
480    /// Requested byte length for the current checkout.
481    pub fn byte_len(&self) -> usize {
482        self.requested_len
483    }
484
485    /// Actual device allocation byte length.
486    pub fn allocation_byte_len(&self) -> usize {
487        self.buffer.as_ref().map_or(0, CudaDeviceBuffer::byte_len)
488    }
489
490    /// Borrow the underlying device buffer while the checkout is live.
491    pub fn as_device_buffer(&self) -> Option<&CudaDeviceBuffer> {
492        self.buffer.as_ref()
493    }
494
495    /// Detach and return the underlying device buffer instead of recycling it
496    /// when this checkout is dropped.
497    pub fn into_device_buffer(mut self) -> Result<CudaDeviceBuffer, CudaError> {
498        self.buffer
499            .take()
500            .ok_or_else(|| CudaError::InvalidArgument {
501                message: "pooled CUDA buffer checkout is empty".to_string(),
502            })
503    }
504
505    /// Copy the requested bytes for this checkout into caller-owned host output.
506    pub fn copy_to_host(&self, out: &mut [u8]) -> Result<(), CudaError> {
507        if out.len() < self.requested_len {
508            return Err(CudaError::OutputTooSmall {
509                required: self.requested_len,
510                have: out.len(),
511            });
512        }
513        if self.requested_len == 0 {
514            return Ok(());
515        }
516        let buffer = self
517            .buffer
518            .as_ref()
519            .ok_or_else(|| CudaError::InvalidArgument {
520                message: "pooled CUDA buffer checkout is empty".to_string(),
521            })?;
522        buffer.context.inner.with_current_resource_operation(|| {
523            // SAFETY: `buffer.ptr` is a live allocation with at least
524            // `requested_len` bytes and `out` covers that range while the
525            // lifecycle gate is held.
526            let result = unsafe {
527                (buffer.context.inner.driver.cu_memcpy_dtoh)(
528                    out.as_mut_ptr().cast::<c_void>(),
529                    buffer.ptr,
530                    self.requested_len,
531                )
532            };
533            buffer.context.inner.driver.check("cuMemcpyDtoH_v2", result)
534        })?;
535        Ok(())
536    }
537}
538
539impl Drop for CudaPooledDeviceBuffer {
540    fn drop(&mut self) {
541        if let Some(buffer) = self.buffer.take() {
542            let _ = self.pool.recycle_buffer(buffer);
543        }
544    }
545}
546
547pub(crate) fn pooled_device_buffer(
548    buffer: &CudaPooledDeviceBuffer,
549) -> Result<&CudaDeviceBuffer, CudaError> {
550    buffer
551        .as_device_buffer()
552        .ok_or_else(|| CudaError::InvalidArgument {
553            message: "pooled CUDA buffer checkout is empty".to_string(),
554        })
555}