j2k_cuda_runtime/memory/pool/
cache_policy.rs1use super::{
4 size_buckets::CudaBufferPoolSizeBuckets, CudaBufferPool, CudaBufferPoolFree,
5 CudaBufferPoolInner, CudaBufferPoolState,
6};
7use crate::{
8 allocation::host_allocation_error, context::CudaContext, error::select_resource_release_error,
9 memory::CudaDeviceBuffer, CudaError,
10};
11use std::sync::{Arc, Mutex};
12
13const DEFAULT_MAX_CACHED_BYTES: usize = 512 * 1024 * 1024;
14const DEFAULT_MAX_CACHED_BUFFERS: usize = 128;
15const DEFAULT_MAX_SIZE_BUCKETS: usize = 64;
16
17#[doc(hidden)]
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct CudaBufferPoolLimits {
21 pub max_cached_bytes: usize,
23 pub max_cached_buffers: usize,
25 pub max_size_buckets: usize,
27}
28
29impl Default for CudaBufferPoolLimits {
30 fn default() -> Self {
31 Self {
32 max_cached_bytes: DEFAULT_MAX_CACHED_BYTES,
33 max_cached_buffers: DEFAULT_MAX_CACHED_BUFFERS,
34 max_size_buckets: DEFAULT_MAX_SIZE_BUCKETS,
35 }
36 }
37}
38
39#[doc(hidden)]
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub struct CudaBufferPoolDiagnostics {
43 pub limits: CudaBufferPoolLimits,
45 pub cached_buffers: usize,
47 pub cached_bytes: usize,
49 pub cached_size_buckets: usize,
51 pub deferred_buffers: usize,
53 pub deferred_bytes: usize,
55 pub reuse_holds: usize,
57 pub peak_cached_buffers: usize,
59 pub peak_cached_bytes: usize,
61 pub peak_cached_size_buckets: usize,
63 pub peak_deferred_buffers: usize,
65 pub peak_deferred_bytes: usize,
67 pub evicted_buffers: usize,
69 pub rejected_buffers: usize,
71 pub metadata_failures: usize,
73}
74
75#[derive(Clone, Copy, Debug, Default)]
76pub(super) struct CudaBufferPoolMetrics {
77 pub(super) peak_cached_buffers: usize,
78 pub(super) peak_cached_bytes: usize,
79 pub(super) peak_cached_size_buckets: usize,
80 pub(super) peak_deferred_buffers: usize,
81 pub(super) peak_deferred_bytes: usize,
82 pub(super) evicted_buffers: usize,
83 pub(super) rejected_buffers: usize,
84 pub(super) metadata_failures: usize,
85}
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88struct CacheInventory {
89 buffers: usize,
90 bytes: usize,
91 size_buckets: usize,
92}
93
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95enum CacheAdmissionDecision {
96 Admit,
97 Evict,
98 Reject,
99}
100
101impl CudaBufferPool {
102 pub fn new(context: CudaContext) -> Self {
104 Self::with_limits(context, CudaBufferPoolLimits::default())
105 }
106
107 #[doc(hidden)]
108 pub fn with_limits(context: CudaContext, limits: CudaBufferPoolLimits) -> Self {
110 Self::with_free(context, limits, CudaBufferPoolFree::FirstFit(Vec::new()))
111 }
112
113 pub(in super::super) fn new_size_buckets(context: CudaContext) -> Self {
114 Self::best_fit_with_limits(context, CudaBufferPoolLimits::default())
115 }
116
117 #[doc(hidden)]
118 pub fn best_fit_with_limits(context: CudaContext, limits: CudaBufferPoolLimits) -> Self {
120 Self::with_free(
121 context,
122 limits,
123 CudaBufferPoolFree::SizeBuckets(CudaBufferPoolSizeBuckets::new()),
124 )
125 }
126
127 fn with_free(
128 context: CudaContext,
129 limits: CudaBufferPoolLimits,
130 free: CudaBufferPoolFree,
131 ) -> Self {
132 Self {
133 inner: Arc::new(CudaBufferPoolInner {
134 context,
135 limits,
136 state: Mutex::new(CudaBufferPoolState {
137 free,
138 deferred: Vec::new(),
139 deferred_bytes: 0,
140 reuse_holds: 0,
141 metrics: CudaBufferPoolMetrics::default(),
142 }),
143 }),
144 }
145 }
146
147 #[doc(hidden)]
148 pub fn diagnostics(&self) -> Result<CudaBufferPoolDiagnostics, CudaError> {
150 self.inner
151 .context
152 .inner
153 .ensure_resource_lifetime_available()?;
154 let state = self
155 .inner
156 .state
157 .lock()
158 .map_err(|error| CudaError::StatePoisoned {
159 message: error.to_string(),
160 })?;
161 let inventory = state.free.inventory();
162 Ok(CudaBufferPoolDiagnostics {
163 limits: self.inner.limits,
164 cached_buffers: inventory.buffers,
165 cached_bytes: inventory.bytes,
166 cached_size_buckets: inventory.size_buckets,
167 deferred_buffers: state.deferred.len(),
168 deferred_bytes: state.deferred_bytes,
169 reuse_holds: state.reuse_holds,
170 peak_cached_buffers: state.metrics.peak_cached_buffers,
171 peak_cached_bytes: state.metrics.peak_cached_bytes,
172 peak_cached_size_buckets: state.metrics.peak_cached_size_buckets,
173 peak_deferred_buffers: state.metrics.peak_deferred_buffers,
174 peak_deferred_bytes: state.metrics.peak_deferred_bytes,
175 evicted_buffers: state.metrics.evicted_buffers,
176 rejected_buffers: state.metrics.rejected_buffers,
177 metadata_failures: state.metrics.metadata_failures,
178 })
179 }
180}
181
182impl CudaBufferPoolInner {
183 pub(super) fn recycle_completed_buffer(
184 &self,
185 buffer: CudaDeviceBuffer,
186 ) -> Result<(), CudaError> {
187 let mut candidate = Some(buffer);
188 loop {
189 let Some(candidate_buffer) = candidate.as_ref() else {
190 return Err(CudaError::InternalInvariant {
191 what: "completed CUDA cache candidate ownership was lost",
192 });
193 };
194 let candidate_bytes = candidate_buffer.byte_len();
195 let mut state = match self.state.lock() {
196 Ok(state) => state,
197 Err(error) => {
198 std::mem::forget(candidate.take());
201 return Err(CudaError::StatePoisoned {
202 message: error.to_string(),
203 });
204 }
205 };
206 let inventory = state.free.inventory();
207 let adds_size_bucket = state.free.candidate_adds_size_bucket(candidate_bytes);
208 match cache_admission_decision(
209 self.limits,
210 inventory,
211 candidate_bytes,
212 adds_size_bucket,
213 ) {
214 CacheAdmissionDecision::Reject => {
215 state.metrics.rejected_buffers =
216 state.metrics.rejected_buffers.saturating_add(1);
217 let rejected = candidate.take();
218 drop(state);
219 drop(rejected);
222 return self.context.inner.ensure_resource_lifetime_available();
223 }
224 CacheAdmissionDecision::Evict => {
225 let Some(evicted) = state.free.evict_deterministic() else {
226 let unretained = candidate.take();
227 drop(state);
228 drop(unretained);
229 return Err(CudaError::InternalInvariant {
230 what: "CUDA buffer cache selected eviction without a victim",
231 });
232 };
233 state.metrics.evicted_buffers = state.metrics.evicted_buffers.saturating_add(1);
234 drop(state);
235 drop(evicted);
238 if let Err(error) = self.context.inner.ensure_resource_lifetime_available() {
239 std::mem::forget(candidate.take());
242 return Err(error);
243 }
244 }
245 CacheAdmissionDecision::Admit => {
246 let Some(admitted) = candidate.take() else {
247 drop(state);
248 return Err(CudaError::InternalInvariant {
249 what: "CUDA buffer cache admitted a missing candidate",
250 });
251 };
252 if let Err((error, unretained)) = state.free.try_recycle(admitted) {
253 state.metrics.metadata_failures =
254 state.metrics.metadata_failures.saturating_add(1);
255 drop(state);
256 drop(unretained);
257 if let Err(release_error) =
258 self.context.inner.ensure_resource_lifetime_available()
259 {
260 return Err(select_resource_release_error(error, release_error));
261 }
262 return Err(error);
263 }
264 observe_cache_high_water(&mut state);
265 return Ok(());
266 }
267 }
268 }
269 }
270}
271
272impl CudaBufferPoolFree {
273 fn inventory(&self) -> CacheInventory {
274 match self {
275 Self::FirstFit(buffers) => CacheInventory {
276 buffers: buffers.len(),
277 bytes: buffers.iter().fold(0usize, |total, buffer| {
278 total.saturating_add(buffer.byte_len())
279 }),
280 size_buckets: 0,
281 },
282 Self::SizeBuckets(buckets) => CacheInventory {
283 buffers: buckets.cached_count(),
284 bytes: buckets.cached_bytes(),
285 size_buckets: buckets.bucket_count(),
286 },
287 }
288 }
289
290 fn candidate_adds_size_bucket(&self, candidate_bytes: usize) -> bool {
291 match self {
292 Self::FirstFit(_) => false,
293 Self::SizeBuckets(buckets) => !buckets.contains_size(candidate_bytes),
294 }
295 }
296
297 fn evict_deterministic(&mut self) -> Option<CudaDeviceBuffer> {
298 match self {
299 Self::FirstFit(buffers) => (!buffers.is_empty()).then(|| buffers.remove(0)),
300 Self::SizeBuckets(buckets) => buckets.evict_largest_oldest(),
301 }
302 }
303
304 fn try_recycle(
305 &mut self,
306 buffer: CudaDeviceBuffer,
307 ) -> Result<(), (CudaError, CudaDeviceBuffer)> {
308 match self {
309 Self::FirstFit(buffers) => {
310 if buffers.try_reserve(1).is_err() {
311 let error =
312 host_allocation_error::<CudaDeviceBuffer>(buffers.len().saturating_add(1));
313 return Err((error, buffer));
314 }
315 buffers.push(buffer);
316 Ok(())
317 }
318 Self::SizeBuckets(buckets) => buckets.try_recycle(buffer),
319 }
320 }
321}
322
323fn cache_admission_decision(
324 limits: CudaBufferPoolLimits,
325 inventory: CacheInventory,
326 candidate_bytes: usize,
327 adds_size_bucket: bool,
328) -> CacheAdmissionDecision {
329 if limits.max_cached_buffers == 0
330 || candidate_bytes > limits.max_cached_bytes
331 || (adds_size_bucket && limits.max_size_buckets == 0)
332 {
333 return CacheAdmissionDecision::Reject;
334 }
335
336 let next_buffers = inventory.buffers.checked_add(1);
337 let next_bytes = inventory.bytes.checked_add(candidate_bytes);
338 let next_buckets = inventory
339 .size_buckets
340 .checked_add(usize::from(adds_size_bucket));
341 let fits = next_buffers.is_some_and(|count| count <= limits.max_cached_buffers)
342 && next_bytes.is_some_and(|bytes| bytes <= limits.max_cached_bytes)
343 && next_buckets.is_some_and(|count| count <= limits.max_size_buckets);
344 if fits {
345 CacheAdmissionDecision::Admit
346 } else if inventory.buffers == 0 {
347 CacheAdmissionDecision::Reject
348 } else {
349 CacheAdmissionDecision::Evict
350 }
351}
352
353pub(super) fn checked_deferred_bytes(current: usize, added: usize) -> Result<usize, CudaError> {
354 current
355 .checked_add(added)
356 .ok_or(CudaError::InternalInvariant {
357 what: "CUDA deferred buffer byte accounting overflow",
358 })
359}
360
361pub(super) fn observe_deferred_high_water(state: &mut CudaBufferPoolState) {
362 state.metrics.peak_deferred_buffers = state
363 .metrics
364 .peak_deferred_buffers
365 .max(state.deferred.len());
366 state.metrics.peak_deferred_bytes = state.metrics.peak_deferred_bytes.max(state.deferred_bytes);
367}
368
369fn observe_cache_high_water(state: &mut CudaBufferPoolState) {
370 let inventory = state.free.inventory();
371 state.metrics.peak_cached_buffers = state.metrics.peak_cached_buffers.max(inventory.buffers);
372 state.metrics.peak_cached_bytes = state.metrics.peak_cached_bytes.max(inventory.bytes);
373 state.metrics.peak_cached_size_buckets = state
374 .metrics
375 .peak_cached_size_buckets
376 .max(inventory.size_buckets);
377}
378
379#[cfg(test)]
380mod tests;