1mod 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#[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 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 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 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 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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
196pub struct CudaBufferPoolTakeTrace {
197 pub requested_len: usize,
199 pub free_count_before: usize,
201 pub scanned_count: usize,
203 pub reused: bool,
205 pub allocation_byte_len: usize,
207}
208
209impl CudaBufferPool {
210 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 pub fn recycle(&self, buffer: CudaDeviceBuffer) -> Result<(), CudaError> {
239 self.inner.recycle_buffer(buffer)
240 }
241
242 #[doc(hidden)]
243 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 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 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 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 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 pub fn upload_f32(&self, samples: &[f32]) -> Result<CudaPooledDeviceBuffer, CudaError> {
352 self.upload(f32_slice_as_bytes(samples))
353 }
354
355 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 pub fn upload_i16(&self, samples: &[i16]) -> Result<CudaPooledDeviceBuffer, CudaError> {
363 self.upload(i16_slice_as_bytes(samples))
364 }
365
366 #[doc(hidden)]
367 pub fn upload_i16_pinned(&self, samples: &[i16]) -> Result<CudaPooledDeviceBuffer, CudaError> {
369 self.upload_pinned(i16_slice_as_bytes(samples))
370 }
371
372 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#[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 pub fn device_ptr(&self) -> u64 {
477 self.buffer.as_ref().map_or(0, CudaDeviceBuffer::device_ptr)
478 }
479
480 pub fn byte_len(&self) -> usize {
482 self.requested_len
483 }
484
485 pub fn allocation_byte_len(&self) -> usize {
487 self.buffer.as_ref().map_or(0, CudaDeviceBuffer::byte_len)
488 }
489
490 pub fn as_device_buffer(&self) -> Option<&CudaDeviceBuffer> {
492 self.buffer.as_ref()
493 }
494
495 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 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 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}