1use std::collections::HashMap;
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::{Arc, Mutex};
12
13use crate::memory::RawDeviceAllocation;
14
15#[cfg(test)]
16use super::resource::AllocTag;
17use super::resource::{
18 Access, AllocationAccounting, AllocationRequest, BlockId, BlockState, DeviceAccessDependencies,
19 DeviceBlock, DeviceMemoryResource, Generation, ResourceBudgetSnapshot, ResourceError,
20 ResourceResult, StreamId,
21};
22use super::stream_pool::StreamPool;
23use crate::CudaDevice;
24
25struct LiveEntry {
31 slice: Arc<RawDeviceAllocation>,
32 generation: Generation,
33 alloc_stream: StreamId,
34 dependencies: Arc<DeviceAccessDependencies>,
38}
39
40pub struct AsyncCudaResource {
42 device: Arc<CudaDevice>,
43 device_ordinal: u32,
44 stream_pool: Arc<StreamPool>,
45 live: Mutex<HashMap<u64, LiveEntry>>,
48 live_bytes: AtomicUsize,
50 outstanding_bytes: Arc<AllocationAccounting>,
53 pending_bytes: Arc<AtomicUsize>,
57 pending_per_stream: Mutex<HashMap<StreamId, Vec<Option<Arc<RawDeviceAllocation>>>>>,
62}
63
64impl Drop for AsyncCudaResource {
65 fn drop(&mut self) {
66 let live = std::mem::take(
71 self.live
72 .get_mut()
73 .unwrap_or_else(|error| error.into_inner()),
74 );
75 let pending = std::mem::take(
76 self.pending_per_stream
77 .get_mut()
78 .unwrap_or_else(|error| error.into_inner()),
79 );
80 drop(live);
85 drop(pending);
86 }
87}
88
89impl AsyncCudaResource {
90 pub fn new(device: Arc<CudaDevice>, device_ordinal: u32, stream_pool: Arc<StreamPool>) -> Self {
94 Self {
95 device,
96 device_ordinal,
97 stream_pool,
98 live: Mutex::new(HashMap::new()),
99 live_bytes: AtomicUsize::new(0),
100 outstanding_bytes: Arc::default(),
101 pending_bytes: Arc::new(AtomicUsize::new(0)),
102 pending_per_stream: Mutex::new(HashMap::new()),
103 }
104 }
105
106 pub fn device(&self) -> &Arc<CudaDevice> {
107 &self.device
108 }
109
110 pub fn stream_pool(&self) -> &Arc<StreamPool> {
111 &self.stream_pool
112 }
113
114 pub fn live_bytes(&self) -> usize {
118 self.live_bytes.load(Ordering::Relaxed)
119 }
120
121 pub fn pending_free_bytes(&self) -> usize {
124 self.pending_bytes.load(Ordering::Relaxed)
125 }
126
127 pub fn pending_per_stream_total(&self) -> usize {
135 let map = self
136 .pending_per_stream
137 .lock()
138 .expect("AsyncCudaResource pending_per_stream poisoned");
139 map.values()
140 .flatten()
141 .flatten()
142 .filter(|owner| !owner.reclamation().was_released())
143 .map(|owner| owner.len())
144 .sum()
145 }
146
147 pub fn pending_use_event_count(&self, ptr: u64) -> Option<usize> {
154 let live = self
155 .live
156 .lock()
157 .expect("AsyncCudaResource live map poisoned");
158 live.get(&ptr)
159 .map(|entry| entry.dependencies.pending_event_count())
160 }
161}
162
163impl DeviceMemoryResource for AsyncCudaResource {
164 fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
165 let AllocationRequest {
166 bytes,
167 stream,
168 tag,
169 reclamation,
170 ..
171 } = request;
172 if bytes == 0 {
173 return Err(ResourceError::Driver(
174 "AsyncCudaResource: zero-byte allocation not supported".to_string(),
175 ));
176 }
177 let cu_stream = self.stream_pool.resolve(stream).ok_or_else(|| {
178 ResourceError::StreamMisuse(format!(
179 "AsyncCudaResource: unknown StreamId({})",
180 stream.0
181 ))
182 })?;
183 reclamation.attach_resource(Arc::clone(&self.outstanding_bytes), bytes)?;
184 let allocation = crate::memory::RawDeviceAllocation::allocate(
185 Arc::clone(&cu_stream),
186 Arc::clone(self.device.inner().allocation_stream()),
187 bytes,
188 None,
189 Arc::clone(&reclamation),
190 )?;
191 let ptr = allocation.ptr();
192 let dependencies = allocation.dependencies();
193 let mut live = self
194 .live
195 .lock()
196 .expect("AsyncCudaResource live map poisoned");
197 if live.contains_key(&ptr) {
198 return Err(
199 ResourceError::Driver(format!("allocation pointer collision: {ptr:#x}"))
200 .retaining(bytes, reclamation),
201 );
202 }
203 let generation = Generation::next();
204 live.insert(
205 ptr,
206 LiveEntry {
207 slice: allocation,
208 generation,
209 alloc_stream: stream,
210 dependencies,
211 },
212 );
213 self.live_bytes.fetch_add(bytes, Ordering::Relaxed);
214 Ok(DeviceBlock {
215 ptr,
216 device_ordinal: self.device_ordinal,
217 alloc_stream: stream,
218 bytes,
219 align: std::mem::align_of::<u8>(),
220 tag,
221 generation,
222 state: BlockState::Live,
223 })
224 }
225
226 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
227 if block.device_ordinal != self.device_ordinal {
228 return Err(ResourceError::Driver(format!(
229 "AsyncCudaResource: deallocate on wrong device (block ord {} vs resource ord {})",
230 block.device_ordinal, self.device_ordinal
231 )));
232 }
233 let _alloc_stream = self
240 .stream_pool
241 .resolve(block.alloc_stream)
242 .ok_or_else(|| {
243 ResourceError::StreamMisuse(format!(
244 "AsyncCudaResource::deallocate: alloc_stream StreamId({}) does not resolve",
245 block.alloc_stream.0
246 ))
247 })?;
248
249 let (slice, dependencies) = {
253 let mut live = self
254 .live
255 .lock()
256 .expect("AsyncCudaResource live map poisoned");
257 match live.get(&block.ptr) {
258 Some(entry) => {
259 BlockId::from_block(&block).validate_allocation(
260 block.bytes,
261 BlockId {
262 ptr: block.ptr,
263 generation: entry.generation,
264 alloc_stream: entry.alloc_stream,
265 device_ordinal: self.device_ordinal,
266 },
267 entry.slice.len(),
268 )?;
269 entry
270 .slice
271 .reclamation()
272 .attach_pending(Arc::clone(&self.pending_bytes), block.bytes)?;
273 let LiveEntry {
274 slice,
275 dependencies,
276 ..
277 } = live
278 .remove(&block.ptr)
279 .expect("present under lock per get above");
280 self.live_bytes.fetch_sub(block.bytes, Ordering::Relaxed);
281 (slice, dependencies)
282 }
283 None => {
284 return Err(ResourceError::UseAfterFree {
285 generation: block.generation,
286 });
287 }
288 }
289 };
290
291 let mut pending = self
294 .pending_per_stream
295 .lock()
296 .expect("pending allocation queue poisoned");
297 pending
298 .entry(block.alloc_stream)
299 .or_default()
300 .push(Some(slice));
301 drop(pending);
302 drop(dependencies);
303 Ok(())
304 }
305
306 fn device_ordinal(&self) -> u32 {
307 self.device_ordinal
308 }
309
310 fn bytes_outstanding(&self) -> usize {
311 self.outstanding_bytes.snapshot().0
312 }
313
314 fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
315 Arc::clone(&self.outstanding_bytes)
316 }
317
318 fn budget_snapshot(&self) -> Option<ResourceBudgetSnapshot> {
319 None
320 }
321
322 fn reap_pending(&self) -> ResourceResult<()> {
323 self.reap_pending_with(|_| Ok(()))
326 }
327
328 fn supports_block_use_tracking(&self) -> bool {
329 true
330 }
331
332 fn record_block_use(&self, block: &DeviceBlock, use_stream: StreamId) -> ResourceResult<()> {
333 self.finish_block_use(BlockId::from_block(block), use_stream, Access::Read)
344 }
345
346 fn access_dependencies(
347 &self,
348 block: BlockId,
349 bytes: usize,
350 ) -> ResourceResult<Option<Arc<DeviceAccessDependencies>>> {
351 let live = self
352 .live
353 .lock()
354 .expect("AsyncCudaResource live map poisoned");
355 let entry = live.get(&block.ptr).ok_or(ResourceError::UseAfterFree {
356 generation: block.generation,
357 })?;
358 block.validate_allocation(
359 bytes,
360 BlockId {
361 ptr: block.ptr,
362 generation: entry.generation,
363 alloc_stream: entry.alloc_stream,
364 device_ordinal: self.device_ordinal,
365 },
366 entry.slice.len(),
367 )?;
368 Ok(Some(Arc::clone(&entry.dependencies)))
369 }
370
371 fn prepare_block_use(
372 &self,
373 block: BlockId,
374 use_stream: StreamId,
375 access: Access,
376 ) -> ResourceResult<()> {
377 if block.device_ordinal != self.device_ordinal {
378 return Err(ResourceError::Driver(format!(
379 "AsyncCudaResource::prepare_block_use: block device {} != resource device {}",
380 block.device_ordinal, self.device_ordinal
381 )));
382 }
383 let use_cu_stream = self.stream_pool.resolve(use_stream).ok_or_else(|| {
384 ResourceError::StreamMisuse(format!(
385 "AsyncCudaResource::prepare_block_use: unknown StreamId({})",
386 use_stream.0
387 ))
388 })?;
389
390 let live = self
397 .live
398 .lock()
399 .expect("AsyncCudaResource live map poisoned");
400 let entry = match live.get(&block.ptr) {
401 Some(entry) if entry.generation == block.generation => entry,
402 Some(_) | None => {
403 return Err(ResourceError::UseAfterFree {
404 generation: block.generation,
405 });
406 }
407 };
408 entry.dependencies.prepare(&use_cu_stream, access)
409 }
410
411 fn finish_block_use(
412 &self,
413 block: BlockId,
414 use_stream: StreamId,
415 access: Access,
416 ) -> ResourceResult<()> {
417 if block.device_ordinal != self.device_ordinal {
418 return Err(ResourceError::Driver(format!(
419 "AsyncCudaResource::finish_block_use: block device {} != resource device {}",
420 block.device_ordinal, self.device_ordinal
421 )));
422 }
423 let use_cu_stream = self.stream_pool.resolve(use_stream).ok_or_else(|| {
424 ResourceError::StreamMisuse(format!(
425 "AsyncCudaResource::finish_block_use: unknown StreamId({})",
426 use_stream.0
427 ))
428 })?;
429 let live = self
432 .live
433 .lock()
434 .expect("AsyncCudaResource live map poisoned");
435 let entry = match live.get(&block.ptr) {
436 Some(entry) if entry.generation == block.generation => entry,
437 _ => {
438 return Err(ResourceError::UseAfterFree {
439 generation: block.generation,
440 });
441 }
442 };
443 entry.dependencies.record_completion(use_cu_stream, access)
446 }
447}
448
449impl AsyncCudaResource {
450 pub(crate) fn reap_pending_with<F>(&self, mut sync_stream: F) -> ResourceResult<()>
458 where
459 F: FnMut(StreamId) -> ResourceResult<()>,
460 {
461 let mut pending = crate::memory::PendingReclamationBatch::take(
469 &self.pending_per_stream,
470 |queue, pending| {
471 for (id, owners) in pending.iter_mut() {
472 owners.retain(Option::is_some);
473 if !owners.is_empty() {
474 queue.entry(*id).or_default().append(owners);
475 }
476 }
477 pending.clear();
478 },
479 );
480 let mut failure = None;
481 for (stream_id, allocations) in pending.iter_mut() {
482 if let Err(error) = sync_stream(*stream_id) {
483 failure = Some(error);
484 break;
485 }
486 if let Err(error) = crate::memory::reap_raw_allocations(allocations) {
487 failure = Some(error);
488 break;
489 }
490 }
491 match failure {
493 Some(error) => Err(error),
494 None => Ok(()),
495 }
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 fn try_setup() -> Option<(Arc<CudaDevice>, Arc<StreamPool>)> {
504 let device = Arc::new(CudaDevice::new(0).ok()?);
505 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
506 Some((device, pool))
507 }
508
509 #[test]
510 fn allocate_then_deallocate_round_trips_on_default_stream() {
511 let Some((device, pool)) = try_setup() else {
512 return;
513 };
514 let r = AsyncCudaResource::new(device, 0, pool);
515 let block = r
516 .allocate(2048, StreamId::DEFAULT, AllocTag::UNTAGGED)
517 .expect("alloc");
518 assert_eq!(block.bytes, 2048);
519 assert_eq!(block.alloc_stream, StreamId::DEFAULT);
520 assert_eq!(r.bytes_outstanding(), 2048);
521 assert_eq!(r.live_bytes(), 2048);
522 assert_eq!(r.pending_free_bytes(), 0);
523
524 r.deallocate(block).expect("dealloc");
525 assert_eq!(r.live_bytes(), 0);
527 assert_eq!(r.pending_free_bytes(), 2048);
528 assert_eq!(r.bytes_outstanding(), 2048);
529
530 r.reap_pending().expect("reap pending");
531 assert_eq!(r.bytes_outstanding(), 0);
532 assert_eq!(r.pending_free_bytes(), 0);
533 }
534
535 #[test]
536 fn allocate_on_acquired_non_default_stream() {
537 let Some((device, pool)) = try_setup() else {
538 return;
539 };
540 let r = AsyncCudaResource::new(device, 0, Arc::clone(&pool));
541 let stream = pool.acquire().expect("acquire non-default stream");
542 let block = r
543 .allocate(1024, stream, AllocTag("async-test"))
544 .expect("alloc on non-default stream");
545 assert_eq!(block.alloc_stream, stream);
546 r.deallocate(block).expect("dealloc");
547 assert_eq!(r.bytes_outstanding(), 1024);
549 r.reap_pending().expect("reap pending");
550 assert_eq!(r.bytes_outstanding(), 0);
551 }
552
553 #[test]
554 fn allocate_unknown_stream_id_rejected() {
555 let Some((device, pool)) = try_setup() else {
556 return;
557 };
558 let r = AsyncCudaResource::new(device, 0, pool);
559 let err = r.allocate(64, StreamId(99), AllocTag::UNTAGGED);
560 assert!(matches!(err, Err(ResourceError::StreamMisuse(_))));
561 }
562
563 #[test]
564 fn deallocate_unknown_block_returns_use_after_free() {
565 let Some((device, pool)) = try_setup() else {
566 return;
567 };
568 let r = AsyncCudaResource::new(device, 0, pool);
569 let bogus = DeviceBlock {
570 ptr: 0xfeed_face,
571 device_ordinal: 0,
572 alloc_stream: StreamId::DEFAULT,
573 bytes: 16,
574 align: 1,
575 tag: AllocTag::UNTAGGED,
576 generation: Generation::next(),
577 state: BlockState::Live,
578 };
579 assert!(matches!(
580 r.deallocate(bogus),
581 Err(ResourceError::UseAfterFree { .. })
582 ));
583 }
584
585 #[test]
586 fn reap_with_no_pending_is_noop() {
587 let Some((device, pool)) = try_setup() else {
588 return;
589 };
590 let r = AsyncCudaResource::new(device, 0, pool);
591 r.reap_pending().expect("reap on empty");
592 assert_eq!(r.bytes_outstanding(), 0);
593 }
594
595 fn install_pending(r: &AsyncCudaResource, entries: &[(StreamId, usize)]) {
600 for (id, bytes) in entries {
601 let block = r
602 .allocate(*bytes, *id, AllocTag::UNTAGGED)
603 .expect("allocate pending owner");
604 r.deallocate(block).expect("retire pending owner");
605 }
606 }
607
608 #[test]
609 fn reap_pending_recovers_unsynced_streams_when_sync_fails() {
610 let Some((device, pool)) = try_setup() else {
614 return;
615 };
616 let r = AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool));
617
618 let first = pool.acquire().expect("first allocation stream");
619 let second = pool.acquire().expect("second allocation stream");
620 assert_eq!((first, second), (StreamId(1), StreamId(2)));
621
622 install_pending(&r, &[(StreamId(1), 1024), (StreamId(2), 2048)]);
625 assert_eq!(r.pending_free_bytes(), 3072);
626 assert_eq!(r.pending_per_stream_total(), 3072);
627
628 let synced = std::sync::Mutex::new(Vec::<StreamId>::new());
633 let result = r.reap_pending_with(|stream_id| {
634 if stream_id == StreamId(2) {
635 Err(ResourceError::Driver(
636 "simulated sync failure on StreamId(2)".into(),
637 ))
638 } else {
639 synced.lock().unwrap().push(stream_id);
640 Ok(())
641 }
642 });
643
644 assert!(matches!(result, Err(ResourceError::Driver(_))));
645
646 let synced = synced.into_inner().unwrap();
647 let synced_bytes: usize = if synced.contains(&StreamId(1)) {
653 1024
654 } else {
655 0
656 };
657 let expected_pending = 3072 - synced_bytes;
658 assert_eq!(
659 r.pending_free_bytes(),
660 expected_pending,
661 "synced={:?}; pending_bytes must reflect only un-synced bytes",
662 synced
663 );
664 assert_eq!(
665 r.pending_per_stream_total(),
666 expected_pending,
667 "synced={:?}; pending_per_stream_total must equal pending_free_bytes \
668 (cross-counter invariant)",
669 synced
670 );
671
672 r.reap_pending_with(|_| Ok(())).expect("retry reap");
676 assert_eq!(r.pending_free_bytes(), 0);
677 assert_eq!(r.pending_per_stream_total(), 0);
678 }
679
680 #[test]
681 fn reap_pending_drains_normally_when_sync_always_succeeds() {
682 let Some((device, pool)) = try_setup() else {
685 return;
686 };
687 let r = AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool));
688
689 let first = pool.acquire().expect("first allocation stream");
690 let second = pool.acquire().expect("second allocation stream");
691 assert_eq!((first, second), (StreamId(1), StreamId(2)));
692
693 install_pending(&r, &[(StreamId(1), 256), (StreamId(2), 512)]);
694 r.reap_pending_with(|_| Ok(())).expect("reap");
695 assert_eq!(r.pending_free_bytes(), 0);
696 assert_eq!(r.pending_per_stream_total(), 0);
697 }
698}