1#[cfg(feature = "cuda-runtime")]
4use j2k_cuda_runtime::{
5 CudaBufferPool, CudaClassicDecodeTableResources, CudaContext, CudaContextDiagnostics,
6 CudaHtj2kDecodeTableResources, CudaHtj2kDecodeTables, CudaHtj2kEncodeResources,
7};
8#[cfg(feature = "cuda-runtime")]
9use j2k_native::{ht_uvlc_table0, ht_uvlc_table1, ht_vlc_table0, ht_vlc_table1};
10#[cfg(feature = "cuda-runtime")]
11use std::num::NonZeroUsize;
12#[cfg(all(test, feature = "cuda-runtime"))]
13use std::sync::atomic::{AtomicUsize, Ordering};
14#[cfg(feature = "cuda-runtime")]
15use std::sync::Arc;
16
17#[cfg(feature = "cuda-runtime")]
18use crate::runtime::cuda_error;
19#[cfg(feature = "cuda-runtime")]
20use crate::Error;
21
22#[cfg(all(test, feature = "cuda-runtime"))]
23static HTJ2K_DECODE_TABLE_UPLOADS: AtomicUsize = AtomicUsize::new(0);
24#[cfg(all(test, feature = "cuda-runtime"))]
25static CLASSIC_DECODE_TABLE_UPLOADS: AtomicUsize = AtomicUsize::new(0);
26
27#[cfg(feature = "cuda-runtime")]
29#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
30pub struct CudaDecodePoolSnapshot {
31 pub cached_buffers: usize,
33 pub cached_bytes: usize,
35 pub deferred_buffers: usize,
37 pub deferred_bytes: usize,
39 pub reuse_holds: usize,
41 pub peak_cached_bytes: usize,
43 pub peak_deferred_bytes: usize,
45}
46
47#[cfg(feature = "cuda-runtime")]
48impl CudaDecodePoolSnapshot {
49 #[must_use]
51 pub const fn retained_bytes(self) -> usize {
52 self.cached_bytes.saturating_add(self.deferred_bytes)
53 }
54
55 #[must_use]
57 pub const fn peak_retained_bytes_upper_bound(self) -> usize {
58 self.peak_cached_bytes
59 .saturating_add(self.peak_deferred_bytes)
60 }
61}
62
63#[cfg(feature = "cuda-runtime")]
64impl From<j2k_cuda_runtime::CudaBufferPoolDiagnostics> for CudaDecodePoolSnapshot {
65 fn from(value: j2k_cuda_runtime::CudaBufferPoolDiagnostics) -> Self {
66 Self {
67 cached_buffers: value.cached_buffers,
68 cached_bytes: value.cached_bytes,
69 deferred_buffers: value.deferred_buffers,
70 deferred_bytes: value.deferred_bytes,
71 reuse_holds: value.reuse_holds,
72 peak_cached_bytes: value.peak_cached_bytes,
73 peak_deferred_bytes: value.peak_deferred_bytes,
74 }
75 }
76}
77
78#[cfg(feature = "cuda-runtime")]
80#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
81pub struct CudaDecodePoolDiagnostics {
82 pub decode: Option<CudaDecodePoolSnapshot>,
84 pub batch_decode: Option<CudaDecodePoolSnapshot>,
86}
87
88#[cfg(feature = "cuda-runtime")]
90#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
91pub struct CudaSessionDiagnostics {
92 pub runtime: Option<CudaContextDiagnostics>,
95 pub pools: CudaDecodePoolDiagnostics,
97}
98
99#[cfg(feature = "cuda-runtime")]
100impl CudaDecodePoolDiagnostics {
101 #[must_use]
103 pub fn retained_bytes(self) -> usize {
104 self.decode
105 .map_or(0, CudaDecodePoolSnapshot::retained_bytes)
106 .saturating_add(
107 self.batch_decode
108 .map_or(0, CudaDecodePoolSnapshot::retained_bytes),
109 )
110 }
111
112 #[must_use]
114 pub fn peak_retained_bytes_upper_bound(self) -> usize {
115 self.decode
116 .map_or(0, CudaDecodePoolSnapshot::peak_retained_bytes_upper_bound)
117 .saturating_add(
118 self.batch_decode
119 .map_or(0, CudaDecodePoolSnapshot::peak_retained_bytes_upper_bound),
120 )
121 }
122}
123
124#[cfg(feature = "cuda-runtime")]
125mod encode_resources;
126
127#[cfg(feature = "cuda-runtime")]
128use self::encode_resources::get_or_try_init_context_bound;
129
130#[derive(Clone, Default)]
132pub struct CudaSession {
133 submissions: u64,
134 #[cfg(feature = "cuda-runtime")]
135 context: Option<CudaContext>,
136 #[cfg(feature = "cuda-runtime")]
137 htj2k_decode_tables: Option<CudaHtj2kDecodeTableResources>,
138 #[cfg(feature = "cuda-runtime")]
139 classic_decode_tables: Option<CudaClassicDecodeTableResources>,
140 #[cfg(feature = "cuda-runtime")]
141 htj2k_encode_resources: Option<Arc<CudaHtj2kEncodeResources>>,
142 #[cfg(all(test, feature = "cuda-runtime"))]
143 htj2k_encode_resource_uploads: usize,
144 #[cfg(feature = "cuda-runtime")]
145 decode_buffer_pool: Option<CudaBufferPool>,
146 #[cfg(feature = "cuda-runtime")]
147 decode_batch_buffer_pool: Option<CudaBufferPool>,
148 #[cfg(feature = "cuda-runtime")]
149 htj2k_decode_chunk_limits: Option<j2k_core::HtGpuJobChunkLimits>,
150 #[cfg(all(test, feature = "cuda-runtime"))]
151 last_htj2k_decode_chunk_count: usize,
152}
153
154impl CudaSession {
155 #[cfg(feature = "cuda-runtime")]
157 #[doc(hidden)]
158 pub fn with_context(context: CudaContext) -> Self {
159 Self {
160 context: Some(context),
161 ..Self::default()
162 }
163 }
164
165 pub fn submissions(&self) -> u64 {
167 self.submissions
168 }
169
170 #[cfg(feature = "cuda-runtime")]
171 pub fn is_runtime_initialized(&self) -> bool {
173 self.context.is_some()
174 }
175
176 #[cfg(feature = "cuda-runtime")]
182 #[doc(hidden)]
183 pub fn context_for_device_interop(
184 &mut self,
185 device_ordinal: usize,
186 ) -> Result<CudaContext, Error> {
187 if let Some(context) = &self.context {
188 if context.device_ordinal() != device_ordinal {
189 return Err(Error::UnsupportedCudaRequest {
190 reason: "J2K CUDA interop device does not match the persistent session",
191 });
192 }
193 return Ok(context.clone());
194 }
195
196 let context = CudaContext::retain_primary(device_ordinal).map_err(cuda_error)?;
197 self.context = Some(context.clone());
198 Ok(context)
199 }
200
201 #[cfg(feature = "cuda-runtime")]
202 pub(crate) fn cuda_context(&mut self) -> Result<CudaContext, Error> {
203 if self.context.is_none() {
204 self.context = Some(CudaContext::system_default().map_err(cuda_error)?);
205 }
206 self.context.clone().ok_or(Error::CudaUnavailable)
207 }
208
209 #[cfg(feature = "cuda-runtime")]
210 pub(crate) fn htj2k_decode_table_resources(
211 &mut self,
212 ) -> Result<CudaHtj2kDecodeTableResources, Error> {
213 if let Some(tables) = &self.htj2k_decode_tables {
214 return Ok(tables.clone());
215 }
216
217 let context = self.cuda_context()?;
218 let tables = CudaHtj2kDecodeTables {
219 vlc_table0: ht_vlc_table0(),
220 vlc_table1: ht_vlc_table1(),
221 uvlc_table0: ht_uvlc_table0(),
222 uvlc_table1: ht_uvlc_table1(),
223 };
224 let resources = context
225 .upload_htj2k_decode_table_resources(tables)
226 .map_err(cuda_error)?;
227 #[cfg(test)]
228 HTJ2K_DECODE_TABLE_UPLOADS.fetch_add(1, Ordering::Relaxed);
229 self.htj2k_decode_tables = Some(resources.clone());
230 Ok(resources)
231 }
232
233 #[cfg(feature = "cuda-runtime")]
234 pub(crate) fn classic_decode_table_resources(
235 &mut self,
236 ) -> Result<CudaClassicDecodeTableResources, Error> {
237 if let Some(tables) = &self.classic_decode_tables {
238 return Ok(tables.clone());
239 }
240 let context = self.cuda_context()?;
241 let tables = context
242 .upload_classic_decode_table_resources()
243 .map_err(cuda_error)?;
244 #[cfg(test)]
245 CLASSIC_DECODE_TABLE_UPLOADS.fetch_add(1, Ordering::Relaxed);
246 self.classic_decode_tables = Some(tables.clone());
247 Ok(tables)
248 }
249
250 #[cfg(feature = "cuda-runtime")]
251 pub(crate) fn htj2k_encode_resources(
252 &mut self,
253 requested_context: &CudaContext,
254 ) -> Result<Arc<CudaHtj2kEncodeResources>, Error> {
255 let (resources, initialized) = get_or_try_init_context_bound(
256 &mut self.context,
257 &mut self.htj2k_encode_resources,
258 requested_context,
259 CudaContext::is_same_context,
260 || Error::UnsupportedCudaRequest {
261 reason: "J2K CUDA encode tile belongs to a different context than the session",
262 },
263 |context| {
264 context
265 .upload_htj2k_encode_resources(crate::encode::cuda_htj2k_encode_tables())
266 .map_err(cuda_error)
267 },
268 )?;
269 #[cfg(test)]
270 if initialized {
271 self.htj2k_encode_resource_uploads =
272 self.htj2k_encode_resource_uploads.saturating_add(1);
273 }
274 #[cfg(not(test))]
275 let _ = initialized;
276 Ok(resources)
277 }
278
279 #[cfg(all(test, feature = "cuda-runtime"))]
280 pub(crate) fn htj2k_encode_resource_uploads_for_test(&self) -> usize {
281 self.htj2k_encode_resource_uploads
282 }
283
284 #[cfg(feature = "cuda-runtime")]
285 pub(crate) fn decode_buffer_pool(&mut self) -> Result<CudaBufferPool, Error> {
286 if let Some(pool) = &self.decode_buffer_pool {
287 return Ok(pool.clone());
288 }
289 let context = self.cuda_context()?;
290 let pool = context.buffer_pool();
291 self.decode_buffer_pool = Some(pool.clone());
292 Ok(pool)
293 }
294
295 #[cfg(feature = "cuda-runtime")]
296 pub(crate) fn decode_batch_buffer_pool(&mut self) -> Result<CudaBufferPool, Error> {
297 if let Some(pool) = &self.decode_batch_buffer_pool {
298 return Ok(pool.clone());
299 }
300 let context = self.cuda_context()?;
301 let pool = context.best_fit_buffer_pool();
302 self.decode_batch_buffer_pool = Some(pool.clone());
303 Ok(pool)
304 }
305
306 #[cfg(feature = "cuda-runtime")]
310 pub fn decode_pool_diagnostics(&self) -> Result<CudaDecodePoolDiagnostics, Error> {
311 Ok(CudaDecodePoolDiagnostics {
312 decode: self
313 .decode_buffer_pool
314 .as_ref()
315 .map(|pool| pool.diagnostics().map(CudaDecodePoolSnapshot::from))
316 .transpose()
317 .map_err(cuda_error)?,
318 batch_decode: self
319 .decode_batch_buffer_pool
320 .as_ref()
321 .map(|pool| pool.diagnostics().map(CudaDecodePoolSnapshot::from))
322 .transpose()
323 .map_err(cuda_error)?,
324 })
325 }
326
327 #[cfg(feature = "cuda-runtime")]
329 pub fn diagnostics(&self) -> Result<CudaSessionDiagnostics, Error> {
330 Ok(CudaSessionDiagnostics {
331 runtime: self
332 .context
333 .as_ref()
334 .map(CudaContext::diagnostics)
335 .transpose()
336 .map_err(cuda_error)?,
337 pools: self.decode_pool_diagnostics()?,
338 })
339 }
340
341 #[cfg(feature = "cuda-runtime")]
342 pub(crate) fn htj2k_decode_chunk_limits(&self) -> j2k_core::HtGpuJobChunkLimits {
343 if let Some(limits) = self.htj2k_decode_chunk_limits {
344 return limits;
345 }
346 let Some(max_jobs) = NonZeroUsize::new(65_536) else {
347 return j2k_core::HtGpuJobChunkLimits::new(NonZeroUsize::MIN, 0, 0);
348 };
349 j2k_core::HtGpuJobChunkLimits::new(
350 max_jobs,
351 64 * 1024 * 1024,
352 max_jobs
353 .get()
354 .saturating_mul(j2k_cuda_runtime::htj2k_cleanup_multi_descriptor_bytes()),
355 )
356 }
357
358 #[cfg(all(test, feature = "cuda-runtime"))]
359 pub(crate) fn set_htj2k_decode_chunk_limits_for_test(
360 &mut self,
361 limits: j2k_core::HtGpuJobChunkLimits,
362 ) {
363 self.htj2k_decode_chunk_limits = Some(limits);
364 }
365
366 #[cfg(all(test, feature = "cuda-runtime"))]
367 pub(crate) fn record_htj2k_decode_chunk_count_for_test(&mut self, count: usize) {
368 self.last_htj2k_decode_chunk_count = count;
369 }
370
371 #[cfg(all(test, feature = "cuda-runtime"))]
372 pub(crate) fn last_htj2k_decode_chunk_count_for_test(&self) -> usize {
373 self.last_htj2k_decode_chunk_count
374 }
375}
376
377impl j2k_core::DeviceSubmitSession for CudaSession {
378 fn record_submit(&mut self) {
379 self.submissions = self.submissions.saturating_add(1);
380 }
381}
382
383#[doc(hidden)]
384impl j2k_core::AcceleratorSession for CudaSession {
385 fn backend_kind(&self) -> j2k_core::BackendKind {
386 j2k_core::BackendKind::Cuda
387 }
388
389 fn execution_stats(&self) -> j2k_core::ExecutionStats {
390 j2k_core::ExecutionStats {
391 submissions: self.submissions,
392 ..j2k_core::ExecutionStats::default()
393 }
394 }
395}
396
397#[cfg(all(test, feature = "cuda-runtime"))]
398pub(crate) fn reset_htj2k_decode_table_uploads_for_test() {
399 HTJ2K_DECODE_TABLE_UPLOADS.store(0, Ordering::Relaxed);
400}
401
402#[cfg(all(test, feature = "cuda-runtime"))]
403pub(crate) fn htj2k_decode_table_uploads_for_test() -> usize {
404 HTJ2K_DECODE_TABLE_UPLOADS.load(Ordering::Relaxed)
405}
406
407#[cfg(all(test, feature = "cuda-runtime"))]
408pub(crate) fn reset_classic_decode_table_uploads_for_test() {
409 CLASSIC_DECODE_TABLE_UPLOADS.store(0, Ordering::Relaxed);
410}
411
412#[cfg(all(test, feature = "cuda-runtime"))]
413pub(crate) fn classic_decode_table_uploads_for_test() -> usize {
414 CLASSIC_DECODE_TABLE_UPLOADS.load(Ordering::Relaxed)
415}
416
417impl std::fmt::Debug for CudaSession {
418 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419 let mut debug = f.debug_struct("CudaSession");
420 debug.field("submissions", &self.submissions);
421 #[cfg(feature = "cuda-runtime")]
422 debug.field("runtime_initialized", &self.is_runtime_initialized());
423 #[cfg(feature = "cuda-runtime")]
424 debug.field(
425 "htj2k_decode_tables_cached",
426 &self.htj2k_decode_tables.is_some(),
427 );
428 #[cfg(feature = "cuda-runtime")]
429 debug.field(
430 "classic_decode_tables_cached",
431 &self.classic_decode_tables.is_some(),
432 );
433 #[cfg(feature = "cuda-runtime")]
434 debug.field(
435 "htj2k_encode_resources_cached",
436 &self.htj2k_encode_resources.is_some(),
437 );
438 #[cfg(feature = "cuda-runtime")]
439 debug.field(
440 "decode_buffer_pool_cached",
441 &self.decode_buffer_pool.is_some(),
442 );
443 #[cfg(feature = "cuda-runtime")]
444 debug.field(
445 "decode_batch_buffer_pool_cached",
446 &self.decode_batch_buffer_pool.is_some(),
447 );
448 debug.finish_non_exhaustive()
449 }
450}
451
452#[cfg(all(test, feature = "cuda-runtime"))]
453mod tests;