Skip to main content

xlog_cuda/device_runtime/
resource.rs

1//! Core [`DeviceMemoryResource`] trait and supporting types.
2//!
3//! Mirrors RMM's `device_memory_resource` shape so a future optional
4//! RMM backend can satisfy the same trait without requiring callers to
5//! change. Stream-ordered: every alloc/dealloc names a stream; cross-
6//! stream reuse requires explicit event-based synchronization.
7
8use std::collections::{BTreeMap, HashMap};
9use std::fmt;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, Mutex, OnceLock};
12
13use cudarc::driver::{CudaContext, CudaEvent, CudaStream};
14
15/// Identifier for a CUDA stream owned by the runtime's stream pool.
16/// Wraps the raw cudarc stream handle the resource will use for
17/// `cuMemAllocAsync` / `cuMemFreeAsync` ordering. Construction goes
18/// through the runtime; do not fabricate.
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
20pub struct StreamId(pub u32);
21
22impl StreamId {
23    /// The "default" pool stream for tests and synchronous codepaths
24    /// that have no other stream context. Production callers should
25    /// always carry a real stream from the executor / kernel launch
26    /// site.
27    pub const DEFAULT: StreamId = StreamId(0);
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33
34    struct StorageDropProbe {
35        drops: Arc<std::sync::atomic::AtomicUsize>,
36        released: Arc<std::sync::atomic::AtomicBool>,
37        registry: std::sync::Weak<Mutex<BlockUseRegistry>>,
38    }
39
40    impl MemoryStorageOwner for StorageDropProbe {
41        fn dependencies(&self) -> ResourceResult<Arc<DeviceAccessDependencies>> {
42            panic!("host ownership test must not initialize CUDA dependencies")
43        }
44    }
45
46    impl Drop for StorageDropProbe {
47        fn drop(&mut self) {
48            if let Some(registry) = self.registry.upgrade() {
49                assert!(
50                    registry.try_lock().is_ok(),
51                    "last storage owner dropped under registry lock"
52                );
53            }
54            self.drops.fetch_add(1, Ordering::SeqCst);
55            self.released.store(true, Ordering::Release);
56        }
57    }
58
59    struct HostOperationOwners {
60        registry: Arc<Mutex<BlockUseRegistry>>,
61        _retained: Vec<RetainedStorageUse>,
62        completion_ready: std::sync::atomic::AtomicBool,
63    }
64
65    impl HostOperationOwners {
66        fn cancel(&self, groups: &[MemoryUseGroup]) -> ResourceResult<()> {
67            assert_eq!(groups.len(), 1);
68            self.registry.lock().unwrap().release_memory_uses(groups[0])
69        }
70    }
71
72    impl crate::launch::RecorderCleanup<MemoryUseGroup> for HostOperationOwners {
73        fn synchronize_retired(&self) -> ResourceResult<()> {
74            if self.completion_ready.load(Ordering::Acquire) {
75                Ok(())
76            } else {
77                Err(ResourceError::Driver(
78                    "host operation completion remains unknown".into(),
79                ))
80            }
81        }
82
83        fn cancel_retired(&self, group: MemoryUseGroup) -> ResourceResult<()> {
84            self.cancel(&[group])
85        }
86    }
87
88    fn admitted_owner_operation() -> (
89        Arc<Mutex<BlockUseRegistry>>,
90        Arc<std::sync::atomic::AtomicUsize>,
91        crate::launch::RecorderTransaction<HostOperationOwners, MemoryUseGroup>,
92    ) {
93        let registry = Arc::new(Mutex::new(BlockUseRegistry::default()));
94        let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
95        let mut external = Vec::new();
96        // Distinct allocation/import wrappers over overlapping address ranges
97        // must both be retained, independent of any wrapper/generation IDs.
98        for range in [
99            span(0x1000, 64, Access::ReadWrite),
100            span(0x1020, 16, Access::ReadWrite),
101        ] {
102            let released = Arc::new(std::sync::atomic::AtomicBool::new(false));
103            let storage: Arc<dyn MemoryStorageOwner> = Arc::new(StorageDropProbe {
104                drops: Arc::clone(&drops),
105                released: Arc::clone(&released),
106                registry: Arc::downgrade(&registry),
107            });
108            registry
109                .lock()
110                .unwrap()
111                .register_storage(7, range, Arc::downgrade(&storage), released);
112            external.push(storage);
113        }
114        let uses = [span(0x1020, 8, Access::Write)];
115        let mut retained = Vec::new();
116        let group = {
117            let mut registry = registry.lock().unwrap();
118            registry
119                .retain_storage_uses(7, &uses, &mut retained)
120                .unwrap();
121            registry.reserve_memory_uses(7, &uses).unwrap()
122        };
123        let operation = crate::launch::RecorderTransaction::from_admitted(
124            Arc::new(HostOperationOwners {
125                registry: Arc::clone(&registry),
126                _retained: retained,
127                completion_ready: std::sync::atomic::AtomicBool::new(false),
128            }),
129            Box::new([group]),
130        );
131        drop(external);
132        assert_eq!(drops.load(Ordering::SeqCst), 0);
133        (registry, drops, operation)
134    }
135
136    #[test]
137    fn completed_operation_releases_all_alias_owners_once_outside_registry() {
138        let (registry, drops, mut operation) = admitted_owner_operation();
139        operation
140            .enqueue_operation_with(
141                || Ok(()),
142                |_| Ok(()),
143                |_| Ok::<(), ResourceError>(()),
144                |_| Ok(()),
145                HostOperationOwners::cancel,
146            )
147            .unwrap();
148        operation
149            .commit_with(
150                |owner, group| owner.cancel(&[group]),
151                |_| Ok(()),
152                HostOperationOwners::cancel,
153            )
154            .unwrap();
155        assert_eq!(drops.load(Ordering::SeqCst), 2);
156        drop(operation);
157        assert_eq!(drops.load(Ordering::SeqCst), 2);
158        registry
159            .lock()
160            .unwrap()
161            .reserve_memory_uses(7, &[span(0x1020, 8, Access::ReadWrite)])
162            .unwrap();
163    }
164
165    #[test]
166    fn unknown_operation_retains_alias_owners_and_blocks_later_conflicting_access() {
167        let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
168        let (registry, drops, mut operation) = admitted_owner_operation();
169        let owner = Arc::downgrade(operation.prepared_owner().unwrap());
170        let result = operation.enqueue_operation_with(
171            || Ok(()),
172            |_| Ok(()),
173            |_| Err::<(), _>(ResourceError::Driver("partial enqueue".into())),
174            |_| Err(ResourceError::Driver("completion unknown".into())),
175            HostOperationOwners::cancel,
176        );
177        assert!(result.is_err());
178        drop(operation);
179        assert_eq!(drops.load(Ordering::SeqCst), 0);
180        assert!(registry
181            .lock()
182            .unwrap()
183            .reserve_memory_uses(7, &[span(0x1020, 8, Access::Read)])
184            .is_err());
185        // Unknown completion retains this same capsule in cold retirement,
186        // just as it retains native owners, without cancelling the reservation.
187        crate::cuda_graph::reap_capture_retirements();
188        assert_eq!(drops.load(Ordering::SeqCst), 0);
189        owner
190            .upgrade()
191            .unwrap()
192            .completion_ready
193            .store(true, Ordering::Release);
194        std::thread::spawn(crate::cuda_graph::reap_capture_retirements)
195            .join()
196            .unwrap();
197        assert!(owner.upgrade().is_none());
198        assert_eq!(drops.load(Ordering::SeqCst), 2);
199        let group = registry
200            .lock()
201            .unwrap()
202            .reserve_memory_uses(7, &[span(0x1020, 8, Access::Read)])
203            .unwrap();
204        registry.lock().unwrap().release_memory_uses(group).unwrap();
205    }
206
207    #[test]
208    fn unknown_dependency_preparation_retains_owners_before_the_launch_callback() {
209        let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
210        let (registry, drops, mut operation) = admitted_owner_operation();
211        let owner = Arc::downgrade(operation.prepared_owner().unwrap());
212        let result = operation.enqueue_operation_with(
213            || Ok(()),
214            |_| Err(ResourceError::Driver("dependency wait failed".into())),
215            |_| -> ResourceResult<()> { panic!("launch must not run after failed dependencies") },
216            |_| Err(ResourceError::Driver("completion unknown".into())),
217            HostOperationOwners::cancel,
218        );
219        assert!(matches!(
220            result,
221            Err(crate::launch::LaunchEnqueueError::PreparationAndCleanup { .. })
222        ));
223        drop(operation);
224        assert_eq!(drops.load(Ordering::SeqCst), 0);
225        assert!(registry
226            .lock()
227            .unwrap()
228            .reserve_memory_uses(7, &[span(0x1020, 8, Access::Write)])
229            .is_err());
230        owner
231            .upgrade()
232            .unwrap()
233            .completion_ready
234            .store(true, Ordering::Release);
235        crate::cuda_graph::reap_capture_retirements();
236        assert!(owner.upgrade().is_none());
237        assert_eq!(drops.load(Ordering::SeqCst), 2);
238    }
239
240    #[test]
241    fn allocation_dependency_lookup_validates_the_full_allocation_identity() {
242        let live = read(0x1000, 3).block;
243        live.validate_allocation(64, live, 64).unwrap();
244        assert!(live.validate_allocation(63, live, 64).is_err());
245        assert!(live.validate_allocation(65, live, 64).is_err());
246        for altered in [
247            BlockId {
248                ptr: 0x2000,
249                ..live
250            },
251            BlockId {
252                generation: Generation(2),
253                ..live
254            },
255            BlockId {
256                alloc_stream: StreamId(4),
257                ..live
258            },
259            BlockId {
260                device_ordinal: 1,
261                ..live
262            },
263        ] {
264            assert!(altered.validate_allocation(64, live, 64).is_err());
265        }
266    }
267
268    #[test]
269    fn completed_reclamation_pruning_preserves_unrelated_operation_identity() {
270        let mut registry = BlockUseRegistry::default();
271        let reclamation = crate::memory::AllocationReclamation::default();
272        let block = DeviceBlock {
273            ptr: 0x1000,
274            bytes: 64,
275            align: 8,
276            device_ordinal: 0,
277            alloc_stream: StreamId::DEFAULT,
278            tag: AllocTag::UNTAGGED,
279            generation: Generation(1),
280            state: BlockState::Live,
281        };
282        let free = registry
283            .reserve_reclamation(
284                7,
285                MemoryUse::new(block.ptr, block.bytes, Access::ReadWrite).unwrap(),
286                reclamation.release_proof(),
287            )
288            .unwrap();
289        let ordinary = registry
290            .reserve_memory_uses(7, &[span(0x2000, 64, Access::Write)])
291            .unwrap();
292        reclamation.complete().unwrap();
293        let replacement = registry
294            .reserve_memory_uses(7, &[span(0x1000, 64, Access::Write)])
295            .unwrap();
296        assert!(!registry.prepared_memory.contains_key(&free));
297        assert!(registry.prepared_memory.contains_key(&ordinary));
298        assert!(registry.prepared_memory.contains_key(&replacement));
299        assert_eq!(registry.prepared_memory.len(), 2);
300        registry.release_memory_uses(ordinary).unwrap();
301        assert!(registry.release_memory_uses(ordinary).is_err());
302        assert!(registry
303            .reserve_memory_uses(7, &[span(0x1000, 64, Access::Read)])
304            .is_err());
305        registry.release_memory_uses(replacement).unwrap();
306    }
307
308    fn read(ptr: u64, generation: u64) -> BlockUse {
309        BlockUse {
310            block: BlockId {
311                ptr,
312                generation: Generation(generation),
313                alloc_stream: StreamId(3),
314                device_ordinal: 0,
315            },
316            bytes: 64,
317            access: Access::Read,
318        }
319    }
320
321    #[test]
322    fn pending_alias_blocks_allocation_retirement_until_exact_group_release() {
323        let mut registry = BlockUseRegistry::default();
324        let allocation = span(0x1000, 64, Access::ReadWrite);
325        let first = registry
326            .reserve_memory_uses(7, &[span(0x1010, 8, Access::Read)])
327            .unwrap();
328        let second = registry
329            .reserve_memory_uses(7, &[span(0x1020, 8, Access::Read)])
330            .unwrap();
331        assert!(registry.reserve_memory_uses(7, &[allocation]).is_err());
332        let other_context = registry.reserve_memory_uses(8, &[allocation]).unwrap();
333        registry.release_memory_uses(other_context).unwrap();
334        registry.release_memory_uses(first).unwrap();
335        assert!(registry.release_memory_uses(first).is_err());
336        assert!(registry.reserve_memory_uses(7, &[allocation]).is_err());
337        registry.release_memory_uses(second).unwrap();
338        let released = registry.reserve_memory_uses(7, &[allocation]).unwrap();
339        registry.release_memory_uses(released).unwrap();
340    }
341
342    fn span(start: u64, bytes: usize, access: Access) -> MemoryUse {
343        MemoryUse::new(start, bytes, access).expect("valid test span")
344    }
345
346    #[test]
347    fn memory_access_groups_compare_storage_ranges_not_alias_identity() {
348        let mut registry = BlockUseRegistry::default();
349        let read = span(0x1000, 64, Access::Read);
350        let first = registry.reserve_memory_uses(7, &[read]).unwrap();
351        let alias = span(0x1020, 64, Access::Read);
352        let second = registry.reserve_memory_uses(7, &[alias]).unwrap();
353        assert!(registry
354            .reserve_memory_uses(7, &[span(0x1030, 8, Access::Write)])
355            .is_err());
356        registry.release_memory_uses(first).unwrap();
357        assert!(registry
358            .reserve_memory_uses(7, &[span(0x1040, 8, Access::Write)])
359            .is_err());
360        registry.release_memory_uses(second).unwrap();
361        let writer = registry
362            .reserve_memory_uses(7, &[span(0x1030, 8, Access::Write)])
363            .unwrap();
364        registry.release_memory_uses(writer).unwrap();
365    }
366
367    #[test]
368    fn peer_copy_reserves_both_contexts_atomically() {
369        let mut registry = BlockUseRegistry::default();
370        let source = span(0x1000, 16, Access::Read);
371        let destination = span(0x1000, 16, Access::Write);
372        let occupied = registry.reserve_memory_uses(8, &[destination]).unwrap();
373        assert!(registry
374            .reserve_context_memory_uses(&[(7, source), (8, destination)])
375            .is_err());
376        let source_writer = registry.reserve_memory_uses(7, &[destination]).unwrap();
377        registry.release_memory_uses(source_writer).unwrap();
378        registry.release_memory_uses(occupied).unwrap();
379        let peer = registry
380            .reserve_context_memory_uses(&[(7, source), (8, destination)])
381            .unwrap();
382        assert!(registry.reserve_memory_uses(7, &[destination]).is_err());
383        assert!(registry.reserve_memory_uses(8, &[source]).is_err());
384        let tail = registry
385            .reserve_memory_uses(8, &[span(0x1010, 16, Access::Write)])
386            .unwrap();
387        registry.release_memory_uses(tail).unwrap();
388        registry.release_memory_uses(peer).unwrap();
389        registry.reserve_memory_uses(7, &[destination]).unwrap();
390        registry.reserve_memory_uses(8, &[source]).unwrap();
391    }
392
393    #[test]
394    fn copy_ranges_reject_overlap_only_in_the_same_context() {
395        let source = span(0x1000, 16, Access::Read);
396        let overlapping = span(0x1008, 16, Access::Write);
397        assert!(source.validate_copy_to(7, overlapping, 7).is_err());
398        assert!(source.validate_copy_to(7, source, 7).is_err());
399        source.validate_copy_to(7, overlapping, 8).unwrap();
400        source
401            .validate_copy_to(7, span(0x1010, 16, Access::Write), 7)
402            .unwrap();
403        let empty = span(0x1000, 0, Access::Write);
404        empty.validate_copy_to(7, empty, 7).unwrap();
405    }
406
407    #[test]
408    fn memory_access_groups_preserve_context_and_disjoint_ranges() {
409        let mut registry = BlockUseRegistry::default();
410        let writer = span(0x1000, 64, Access::Write);
411        let first = registry.reserve_memory_uses(7, &[writer]).unwrap();
412        let other_context = registry.reserve_memory_uses(8, &[writer]).unwrap();
413        let adjacent = registry
414            .reserve_memory_uses(7, &[span(0x1040, 8, Access::Write)])
415            .unwrap();
416        let empty = registry
417            .reserve_memory_uses(7, &[span(0x1010, 0, Access::Write)])
418            .unwrap();
419        for group in [first, other_context, adjacent, empty] {
420            registry.release_memory_uses(group).unwrap();
421        }
422    }
423
424    #[test]
425    fn memory_access_group_admission_and_release_are_atomic() {
426        let mut registry = BlockUseRegistry::default();
427        let first = registry
428            .reserve_memory_uses(7, &[span(0x1000, 64, Access::Write)])
429            .unwrap();
430        assert!(registry
431            .reserve_memory_uses(
432                7,
433                &[
434                    span(0x2000, 64, Access::Write),
435                    span(0x1030, 64, Access::Read),
436                ],
437            )
438            .is_err());
439        // A failure in the last member must not retain the earlier member.
440        let second = registry
441            .reserve_memory_uses(7, &[span(0x2000, 64, Access::Write)])
442            .unwrap();
443        registry.release_memory_uses(second).unwrap();
444        assert!(registry.release_memory_uses(second).is_err());
445        assert!(registry
446            .reserve_memory_uses(7, &[span(0x1000, 64, Access::Read)])
447            .is_err());
448        registry.release_memory_uses(first).unwrap();
449        assert!(MemoryUse::new(u64::MAX, 1, Access::Read).is_err());
450    }
451
452    #[test]
453    fn memory_access_group_preserves_per_range_access_with_internal_aliases() {
454        let mut registry = BlockUseRegistry::default();
455        let group = registry
456            .reserve_memory_uses(
457                7,
458                &[
459                    span(0x1000, 64, Access::Read),
460                    span(0x1020, 16, Access::Write),
461                    span(0x2000, 64, Access::Read),
462                ],
463            )
464            .unwrap();
465        let reader = registry
466            .reserve_memory_uses(7, &[span(0x1000, 16, Access::Read)])
467            .unwrap();
468        assert!(registry
469            .reserve_memory_uses(7, &[span(0x1020, 16, Access::Read)])
470            .is_err());
471        // Do not turn the gap between two arguments into an exclusive span.
472        let gap = registry
473            .reserve_memory_uses(7, &[span(0x1800, 64, Access::Write)])
474            .unwrap();
475        for token in [group, reader, gap] {
476            registry.release_memory_uses(token).unwrap();
477        }
478    }
479
480    #[test]
481    fn access_dependencies_keep_published_writes_visible_to_another_stream() {
482        let mut dependencies = AccessDependencies::after_write((7, 1), "allocated");
483        let retired = dependencies.publish((7, 1), "written", Access::Write);
484        drop(retired);
485        let mut waited = Vec::new();
486        dependencies
487            .prepare(&(7, 2), Access::Read, |event| {
488                waited.push(*event);
489                Ok(())
490            })
491            .unwrap();
492        assert_eq!(waited, ["written"]);
493    }
494
495    #[test]
496    fn access_dependencies_preserve_other_streams_until_order_is_proved() {
497        let mut dependencies = AccessDependencies::after_write((7, 1), "written");
498        assert!(dependencies.publish((7, 2), "read", Access::Read).is_none());
499        let mut waited = Vec::new();
500        dependencies
501            .prepare(&(7, 3), Access::Write, |event| {
502                waited.push(*event);
503                Ok(())
504            })
505            .unwrap();
506        assert_eq!(waited, ["written", "read"]);
507        assert!(dependencies
508            .publish((7, 3), "rewritten", Access::Write)
509            .is_none());
510        assert_eq!(dependencies.outstanding_reads, [((7, 2), "read")]);
511        let mut waited = Vec::new();
512        dependencies
513            .prepare(&(7, 4), Access::Read, |event| {
514                waited.push(*event);
515                Ok(())
516            })
517            .unwrap();
518        assert_eq!(waited, ["written", "rewritten"]);
519    }
520
521    #[test]
522    fn access_dependencies_skip_only_the_exact_same_stream() {
523        let dependencies = AccessDependencies::after_write((7, 1), "written");
524        dependencies
525            .prepare(&(7, 1), Access::Read, |_| {
526                panic!("same-stream order needs no wait")
527            })
528            .unwrap();
529        let mut waits = 0;
530        dependencies
531            .prepare(&(8, 1), Access::Read, |_| {
532                waits += 1;
533                Ok(())
534            })
535            .unwrap();
536        assert_eq!(
537            waits, 1,
538            "a pool-local stream index is not a stream identity"
539        );
540    }
541
542    #[test]
543    fn access_dependencies_wait_failure_keeps_all_published_events() {
544        let mut dependencies = AccessDependencies::after_write((7, 1), "written");
545        dependencies.publish((7, 2), "read", Access::Read);
546        assert!(dependencies
547            .prepare(&(7, 3), Access::Write, |_| {
548                Err(ResourceError::Driver("wait failure".into()))
549            })
550            .is_err());
551        assert_eq!(dependencies.outstanding_writes, [((7, 1), "written")]);
552        assert_eq!(dependencies.outstanding_reads, [((7, 2), "read")]);
553    }
554
555    #[test]
556    fn disjoint_operations_do_not_erase_a_concurrently_published_dependency() {
557        let mut registry = BlockUseRegistry::default();
558        let reader = registry
559            .reserve_memory_uses(7, &[span(0x1000, 16, Access::Read)])
560            .unwrap();
561        let writer = registry
562            .reserve_memory_uses(7, &[span(0x1010, 16, Access::Write)])
563            .unwrap();
564        let mut dependencies = AccessDependencies::after_write(0, "allocated");
565        // Both operations prepare before either completion is published. The
566        // writer's event therefore does not imply completion of this reader.
567        dependencies.prepare(&1, Access::Read, |_| Ok(())).unwrap();
568        dependencies.prepare(&2, Access::Write, |_| Ok(())).unwrap();
569        dependencies.publish(1, "left read", Access::Read);
570        dependencies.publish(2, "right write", Access::Write);
571        registry.release_memory_uses(reader).unwrap();
572        registry.release_memory_uses(writer).unwrap();
573        let mut waited = Vec::new();
574        dependencies
575            .prepare(&3, Access::Write, |event| {
576                waited.push(*event);
577                Ok(())
578            })
579            .unwrap();
580        assert!(waited.contains(&"left read"));
581        assert!(waited.contains(&"right write"));
582    }
583
584    #[test]
585    fn disjoint_writers_keep_both_completion_frontiers() {
586        let mut dependencies = AccessDependencies::after_write(0, "allocated");
587        dependencies.prepare(&1, Access::Write, |_| Ok(())).unwrap();
588        dependencies.prepare(&2, Access::Write, |_| Ok(())).unwrap();
589        dependencies.publish(1, "left write", Access::Write);
590        dependencies.publish(2, "right write", Access::Write);
591        let mut waited = Vec::new();
592        dependencies
593            .prepare(&3, Access::Read, |event| {
594                waited.push(*event);
595                Ok(())
596            })
597            .unwrap();
598        assert!(waited.contains(&"left write"));
599        assert!(waited.contains(&"right write"));
600    }
601}
602
603/// Caller-supplied tag for allocation log lines. Short-lived strings
604/// are interned by the logging resource; long-lived borrows are not
605/// retained.
606#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
607pub struct AllocTag(pub &'static str);
608
609impl AllocTag {
610    pub const UNTAGGED: AllocTag = AllocTag("untagged");
611}
612
613/// One allocation admission carried unchanged through every resource decorator.
614/// The terminal backend retains its reclamation ticket with the actual storage.
615pub struct AllocationRequest {
616    pub bytes: usize,
617    pub stream: StreamId,
618    pub tag: AllocTag,
619    /// Bytes promised by the runtime but not yet physically allocated.
620    pub reservation_pressure_bytes: usize,
621    pub(crate) reclamation: Arc<crate::memory::AllocationReclamation>,
622}
623
624impl AllocationRequest {
625    pub fn new(bytes: usize, stream: StreamId, tag: AllocTag) -> Self {
626        Self {
627            bytes,
628            stream,
629            tag,
630            reservation_pressure_bytes: 0,
631            reclamation: Arc::default(),
632        }
633    }
634
635    /// The same lifecycle ticket used by admission and the physical owner.
636    pub fn reclamation(&self) -> Arc<crate::memory::AllocationReclamation> {
637        Arc::clone(&self.reclamation)
638    }
639}
640
641#[derive(Debug, Default)]
642struct AllocationAccountingState {
643    outstanding: usize,
644    total_reclaimed: u128,
645}
646
647/// Exact physical accounting for one terminal resource and all its decorators.
648/// Storage owners publish acquisition and proven release through their tickets.
649/// This ledger owns no allocations and has no separate reclamation queue.
650#[derive(Debug, Default)]
651pub struct AllocationAccounting {
652    state: Mutex<AllocationAccountingState>,
653}
654
655impl AllocationAccounting {
656    /// Atomically snapshot outstanding bytes and cumulative proven releases.
657    pub fn snapshot(&self) -> (usize, u128) {
658        let state = self.state.lock().expect("allocation accounting poisoned");
659        (state.outstanding, state.total_reclaimed)
660    }
661
662    /// Publish one acquisition through its lifecycle ticket.
663    /// Arithmetic failure leaves the complete ledger unchanged.
664    pub(crate) fn acquire(&self, bytes: usize) -> ResourceResult<()> {
665        let mut state = self.state.lock().expect("allocation accounting poisoned");
666        let outstanding = state.outstanding.checked_add(bytes).ok_or_else(|| {
667            ResourceError::Driver("resource allocation accounting overflow".into())
668        })?;
669        state.outstanding = outstanding;
670        Ok(())
671    }
672
673    /// Publish a proven physical release through its lifecycle ticket.
674    /// Outstanding bytes and the cumulative release total change atomically.
675    pub(crate) fn release(&self, bytes: usize) -> ResourceResult<()> {
676        let mut state = self.state.lock().expect("allocation accounting poisoned");
677        let outstanding = state.outstanding.checked_sub(bytes).ok_or_else(|| {
678            ResourceError::Driver("resource release exceeds outstanding bytes".into())
679        })?;
680        let total_reclaimed = state
681            .total_reclaimed
682            .checked_add(bytes as u128)
683            .ok_or_else(|| {
684                ResourceError::Driver("resource reclamation accounting overflow".into())
685            })?;
686        state.outstanding = outstanding;
687        state.total_reclaimed = total_reclaimed;
688        Ok(())
689    }
690}
691
692/// Monotonic counter for distinguishing reuse of the same byte address
693/// across drop / reallocate cycles. Logging and debug-guard resources
694/// use this to detect use-after-free.
695#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
696pub struct Generation(pub u64);
697
698static GENERATION_COUNTER: AtomicU64 = AtomicU64::new(1);
699
700impl Generation {
701    /// Allocate a fresh, monotonically increasing generation number.
702    /// Concurrent calls return distinct values.
703    pub fn next() -> Generation {
704        Generation(GENERATION_COUNTER.fetch_add(1, Ordering::Relaxed))
705    }
706}
707
708/// Access kind for a single block use. Drives the cross-stream
709/// dependency edges the resource queues during
710/// [`DeviceMemoryResource::prepare_block_use`] and the events it
711/// records during [`DeviceMemoryResource::finish_block_use`].
712///
713///   * [`Access::Read`] — the work consumes the block's bytes.
714///     Must wait on any prior write on a different stream. The
715///     resulting event advances that stream's read frontier so future writers
716///     (and the eventual deallocate) can wait on it.
717///   * [`Access::Write`] — the work overwrites the block's bytes
718///     unconditionally. Must wait on the block's prior write AND
719///     all outstanding reads on different streams. The resulting
720///     event advances that stream's write frontier. Other streams' concurrent
721///     uses remain visible until their completion is independently ordered.
722///   * [`Access::ReadWrite`] — both. Same wait set as `Write`,
723///     and the resulting event likewise advances its write frontier.
724#[derive(Clone, Copy, Debug, Eq, PartialEq)]
725pub enum Access {
726    Read,
727    Write,
728    ReadWrite,
729}
730
731impl Access {
732    /// Whether work of this access kind reads the block's bytes.
733    pub fn reads(self) -> bool {
734        matches!(self, Access::Read | Access::ReadWrite)
735    }
736
737    /// Whether work of this access kind writes the block's bytes.
738    pub fn writes(self) -> bool {
739        matches!(self, Access::Write | Access::ReadWrite)
740    }
741}
742
743/// Access-aware event history shared by the allocator and every storage alias.
744/// Disjoint subranges may prepare concurrently, so a writer's publication does
745/// not subsume another stream's later publication. Only the same ordered stream
746/// frontier is replaced. Record and publication must be serialized by the owner.
747struct AccessDependencies<K, E> {
748    outstanding_writes: Vec<(K, E)>,
749    outstanding_reads: Vec<(K, E)>,
750}
751
752impl<K: PartialEq, E> AccessDependencies<K, E> {
753    #[cfg(test)]
754    fn after_write(stream: K, event: E) -> Self {
755        Self {
756            outstanding_writes: vec![(stream, event)],
757            outstanding_reads: Vec::new(),
758        }
759    }
760
761    fn prepare(
762        &self,
763        stream: &K,
764        access: Access,
765        mut wait: impl FnMut(&E) -> ResourceResult<()>,
766    ) -> ResourceResult<()> {
767        for (writer, event) in &self.outstanding_writes {
768            if writer != stream {
769                wait(event)?;
770            }
771        }
772        if access.writes() {
773            for (reader, event) in &self.outstanding_reads {
774                if reader != stream {
775                    wait(event)?;
776                }
777            }
778        }
779        Ok(())
780    }
781
782    /// Return retired events to the caller so their destruction occurs after
783    /// releasing the enclosing state mutex.
784    fn publish(&mut self, stream: K, event: E, access: Access) -> Option<Self> {
785        let mut retired = Self {
786            outstanding_writes: Vec::new(),
787            outstanding_reads: Vec::new(),
788        };
789        if let Some(index) = self
790            .outstanding_reads
791            .iter()
792            .position(|(key, _)| *key == stream)
793        {
794            retired
795                .outstanding_reads
796                .push(self.outstanding_reads.swap_remove(index));
797        }
798        if access.writes() {
799            if let Some(index) = self
800                .outstanding_writes
801                .iter()
802                .position(|(key, _)| *key == stream)
803            {
804                retired
805                    .outstanding_writes
806                    .push(self.outstanding_writes.swap_remove(index));
807            }
808            self.outstanding_writes.push((stream, event));
809        } else {
810            self.outstanding_reads.push((stream, event));
811        }
812        if retired.outstanding_reads.is_empty() && retired.outstanding_writes.is_empty() {
813            None
814        } else {
815            Some(retired)
816        }
817    }
818}
819
820struct RetainedAccessEvent {
821    event: CudaEvent,
822    // Keep the exact stream/context alive until the last dependency is retired.
823    _stream: Arc<CudaStream>,
824}
825
826/// Shared, access-aware CUDA dependencies for an allocation. Resource wrappers
827/// forward this same owner; they must not reconstruct its event history.
828#[doc(hidden)]
829pub struct DeviceAccessDependencies {
830    _allocation_context: Arc<CudaContext>,
831    state: Mutex<AccessDependencies<u64, Arc<RetainedAccessEvent>>>,
832    pub(crate) reclamation: Arc<crate::memory::AllocationReclamation>,
833    allocation: std::sync::OnceLock<std::sync::Weak<crate::memory::RawDeviceAllocation>>,
834}
835
836impl DeviceAccessDependencies {
837    pub(crate) fn bind_allocation(&self, allocation: &Arc<crate::memory::RawDeviceAllocation>) {
838        assert!(self.allocation.set(Arc::downgrade(allocation)).is_ok());
839    }
840
841    /// Upgrade the actual allocation, not a runtime or dependency proxy. The
842    /// caller validates the block identity before this atomic owner acquisition.
843    pub(crate) fn retain_allocation(
844        &self,
845    ) -> ResourceResult<Arc<crate::memory::RawDeviceAllocation>> {
846        self.allocation
847            .get()
848            .and_then(std::sync::Weak::upgrade)
849            .ok_or_else(|| {
850                ResourceError::StreamMisuse(
851                    "device allocation has entered physical retirement".into(),
852                )
853            })
854    }
855
856    pub(crate) fn mark_allocation_released(&self) -> ResourceResult<()> {
857        self.reclamation.complete()
858    }
859
860    pub(crate) fn allocation_was_released(&self) -> bool {
861        self.reclamation.was_released()
862    }
863    /// Cold final-owner retirement waits on recorded events, not on a raw
864    /// per-thread stream handle that may name a different producer thread.
865    pub(crate) fn synchronize(&self) -> ResourceResult<()> {
866        let fences = {
867            let state = self.state.lock().map_err(|_| {
868                ResourceError::Driver(
869                    "cannot prove completion from poisoned device dependency state".into(),
870                )
871            })?;
872            state
873                .outstanding_writes
874                .iter()
875                .chain(&state.outstanding_reads)
876                .map(|(_, fence)| Arc::clone(fence))
877                .collect::<Vec<_>>()
878        };
879        for fence in fences {
880            fence.event.synchronize()?;
881        }
882        Ok(())
883    }
884
885    /// The producer has completed its handoff before publication. XLOG accesses
886    /// after that handoff populate this same history.
887    pub(crate) fn after_ready(
888        context: Arc<CudaContext>,
889        reclamation: Arc<crate::memory::AllocationReclamation>,
890    ) -> Self {
891        Self {
892            reclamation,
893            allocation: std::sync::OnceLock::new(),
894            _allocation_context: context,
895            state: Mutex::new(AccessDependencies {
896                outstanding_writes: Vec::new(),
897                outstanding_reads: Vec::new(),
898            }),
899        }
900    }
901    pub(crate) fn pending_event_count(&self) -> usize {
902        let state = self
903            .state
904            .lock()
905            .expect("device access dependencies poisoned");
906        state.outstanding_reads.len() + state.outstanding_writes.len()
907    }
908
909    pub(crate) fn prepare(&self, stream: &Arc<CudaStream>, access: Access) -> ResourceResult<()> {
910        let execution_id = crate::cuda_graph::stream_execution_id(stream)?;
911        let state = self
912            .state
913            .lock()
914            .expect("device access dependencies poisoned");
915        state.prepare(&execution_id, access, |fence| {
916            stream.wait(&fence.event).map_err(|error| {
917                ResourceError::Driver(format!("device access dependency wait failed: {error}"))
918            })
919        })
920    }
921
922    pub(crate) fn record_completion(
923        &self,
924        stream: Arc<CudaStream>,
925        access: Access,
926    ) -> ResourceResult<()> {
927        // Peer copies publish the destination stream's completion as a source
928        // read dependency. Events belong to the recording context, which need
929        // not be the allocation context; cross-context event waits are valid.
930        let execution_id = crate::cuda_graph::stream_execution_id(&stream)?;
931        let event = stream.context().new_event(None)?;
932        let mut state = self
933            .state
934            .lock()
935            .expect("device access dependencies poisoned");
936        // Serialize the actual record with frontier replacement. Otherwise a
937        // delayed host publisher could replace a newer same-stream event with
938        // its older event, losing work already submitted by another host thread.
939        if let Err(error) = event.record(&stream) {
940            drop(state);
941            return Err(ResourceError::Driver(format!(
942                "device access completion event record failed: {error}"
943            )));
944        }
945        let retired = state.publish(
946            execution_id,
947            Arc::new(RetainedAccessEvent {
948                event,
949                _stream: stream,
950            }),
951            access,
952        );
953        drop(state);
954        drop(retired);
955        Ok(())
956    }
957
958    /// Publish one completion event to every allocation frontier touched by
959    /// an admitted operation.
960    pub(crate) fn record_operation_completion(
961        dependencies: &[(Arc<Self>, Access)],
962        stream: Arc<CudaStream>,
963    ) -> ResourceResult<()> {
964        if dependencies.is_empty() {
965            return Ok(());
966        }
967        let execution_id = crate::cuda_graph::stream_execution_id(&stream)?;
968        let event = stream.context().new_event(None)?;
969        let mut ordered = dependencies.to_vec();
970        ordered.sort_unstable_by_key(|(dependency, _)| Arc::as_ptr(dependency) as usize);
971        let mut combined = Vec::<(Arc<Self>, Access)>::with_capacity(ordered.len());
972        for (dependency, access) in ordered {
973            if let Some((existing, existing_access)) = combined.last_mut() {
974                if Arc::ptr_eq(existing, &dependency) {
975                    *existing_access = if existing_access.writes() || access.writes() {
976                        Access::ReadWrite
977                    } else {
978                        Access::Read
979                    };
980                    continue;
981                }
982            }
983            combined.push((dependency, access));
984        }
985        // Lock every frontier in address order before recording. This keeps
986        // publication atomic without allowing two multi-allocation operations
987        // to acquire the same frontier set in opposite orders.
988        let mut states = Vec::with_capacity(combined.len());
989        for (dependency, access) in &combined {
990            states.push((
991                dependency
992                    .state
993                    .lock()
994                    .expect("device access dependencies poisoned"),
995                *access,
996            ));
997        }
998        if let Err(error) = event.record(&stream) {
999            drop(states);
1000            return Err(ResourceError::Driver(format!(
1001                "device access completion event record failed: {error}"
1002            )));
1003        }
1004        // One stream event proves completion of the whole admitted operation;
1005        // each allocation frontier retains the same event and stream owner.
1006        let event = Arc::new(RetainedAccessEvent {
1007            event,
1008            _stream: stream,
1009        });
1010        let retired = states
1011            .iter_mut()
1012            .filter_map(|(state, access)| state.publish(execution_id, Arc::clone(&event), *access))
1013            .collect::<Vec<_>>();
1014        drop(states);
1015        drop(retired);
1016        Ok(())
1017    }
1018}
1019
1020/// Compact identity of a [`DeviceBlock`] suitable for snapshotting
1021/// into structures whose lifetime should not be tied to the source
1022/// slice's borrow. The fields needed to validate `(ptr, generation)`
1023/// against the resource's live map and to resolve `alloc_stream` for
1024/// cross-stream waits / dealloc ordering.
1025///
1026/// Created via [`BlockId::from_block`]. Pure data; no resource
1027/// handle, no `Drop`. Cheap to copy.
1028#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1029pub struct BlockId {
1030    pub ptr: u64,
1031    pub generation: Generation,
1032    pub alloc_stream: StreamId,
1033    pub device_ordinal: u32,
1034}
1035
1036impl BlockId {
1037    pub(crate) fn validate_allocation(
1038        self,
1039        bytes: usize,
1040        actual: Self,
1041        actual_bytes: usize,
1042    ) -> ResourceResult<()> {
1043        if self != actual || bytes != actual_bytes {
1044            return Err(ResourceError::UseAfterFree {
1045                generation: self.generation,
1046            });
1047        }
1048        Ok(())
1049    }
1050
1051    /// Snapshot a [`DeviceBlock`]'s identity. The returned id is
1052    /// independent of the original block's borrow lifetime; the
1053    /// runtime's generation guard catches stale ids whose backing
1054    /// allocation has been recycled.
1055    pub fn from_block(block: &DeviceBlock) -> Self {
1056        Self {
1057            ptr: block.ptr,
1058            generation: block.generation,
1059            alloc_stream: block.alloc_stream,
1060            device_ordinal: block.device_ordinal,
1061        }
1062    }
1063}
1064
1065/// State of an outstanding [`DeviceBlock`] from the runtime's
1066/// perspective. Adaptors flip blocks between these states; bug-detection
1067/// resources reject operations on blocks in an unexpected state.
1068#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1069pub enum BlockState {
1070    /// Returned from `allocate`; safe to read/write on `alloc_stream`
1071    /// or after a synchronization to another stream.
1072    Live,
1073    /// Returned from `deallocate` but still pending kernel completion
1074    /// on its owning stream. Reuse must wait for stream sync.
1075    Retired,
1076    /// Held by `DebugGuardResource` for delayed reuse / canary
1077    /// validation. Not reissued until the quarantine window passes.
1078    Quarantined,
1079    /// Memory has been physically freed. Any further use is a bug.
1080    Freed,
1081}
1082
1083/// One outstanding device-memory allocation. Owned by the caller until
1084/// returned to its originating resource via
1085/// [`DeviceMemoryResource::deallocate`].
1086///
1087/// Carries the metadata required for stream-ordered correctness and
1088/// post-mortem debugging: the resource that owns the block, the device
1089/// ordinal, the stream the allocation is bound to, byte size, alignment,
1090/// caller tag, generation number, and current state.
1091#[derive(Debug)]
1092pub struct DeviceBlock {
1093    /// Raw device pointer (opaque to safe Rust callers).
1094    pub ptr: u64,
1095    /// CUDA ordinal of the device this block lives on.
1096    pub device_ordinal: u32,
1097    /// Allocation stream. Reads/writes on a different stream require
1098    /// explicit synchronization (event wait or device sync).
1099    pub alloc_stream: StreamId,
1100    /// Size in bytes. May exceed the caller-requested size when the
1101    /// resource rounds up for alignment or pool granularity.
1102    pub bytes: usize,
1103    /// Alignment in bytes (always ≥ caller request).
1104    pub align: usize,
1105    /// Caller-supplied tag, surfaced in allocation logs.
1106    pub tag: AllocTag,
1107    /// Monotonic generation. Reused addresses get fresh generations.
1108    pub generation: Generation,
1109    /// Current state. Adaptors transition this; tests assert on it.
1110    pub state: BlockState,
1111}
1112
1113/// Errors returned by resource implementations. Distinct variants for
1114/// the cases stress tests need to pin (out-of-budget vs CUDA driver
1115/// failure vs use-after-free etc.).
1116#[derive(Debug)]
1117pub enum ResourceError {
1118    /// Allocation succeeded but a later step failed. Its actual owner retains
1119    /// unresolved cleanup independently of this diagnostic. The same ticket
1120    /// reports later reclamation; discarding the error does not abandon storage.
1121    AllocationRetained(RetainedAllocation),
1122    /// The requested allocation would exceed the resource's budget.
1123    /// Carries the exact accounting state captured at the rejecting
1124    /// decision point so callers can report cumulative pressure.
1125    OutOfBudget {
1126        requested: usize,
1127        current: usize,
1128        remaining: usize,
1129        limit: usize,
1130    },
1131    /// CUDA driver returned an error. Carries the wrapped message.
1132    Driver(String),
1133    /// A stream-ordered contract was violated (e.g. dealloc on a
1134    /// stream that does not match the alloc stream without an
1135    /// intervening sync).
1136    StreamMisuse(String),
1137    /// A debug-guard or logging adaptor detected a use-after-free or
1138    /// double-free. Hard error in debug builds; surfaced upward.
1139    UseAfterFree { generation: Generation },
1140    /// A debug-guard adaptor detected an out-of-bounds write past a
1141    /// canary boundary.
1142    OutOfBounds { generation: Generation },
1143}
1144
1145impl fmt::Display for ResourceError {
1146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1147        match self {
1148            Self::AllocationRetained(owner) => write!(f, "allocation of {} bytes failed after acquisition: {}", owner.bytes, owner.cause),
1149            Self::OutOfBudget {
1150                requested,
1151                current,
1152                remaining,
1153                limit,
1154            } => write!(
1155                f,
1156                "out of budget: current {} bytes, requested {} bytes, required {} bytes, limit {} bytes, remaining {} bytes",
1157                current,
1158                requested,
1159                *current as u128 + *requested as u128,
1160                limit,
1161                remaining,
1162            ),
1163            Self::Driver(msg) => write!(f, "CUDA driver error: {}", msg),
1164            Self::StreamMisuse(msg) => write!(f, "stream-ordered contract violated: {}", msg),
1165            Self::UseAfterFree { generation } => {
1166                write!(f, "use-after-free on generation {:?}", generation)
1167            }
1168            Self::OutOfBounds { generation } => {
1169                write!(f, "out-of-bounds write on generation {:?}", generation)
1170            }
1171        }
1172    }
1173}
1174
1175impl std::error::Error for ResourceError {}
1176
1177/// Diagnostic sharing the allocation's lifecycle ticket. The actual storage
1178/// owner remains governed by its cold reaper, independently of this error.
1179pub struct RetainedAllocation {
1180    bytes: usize,
1181    cause: Box<ResourceError>,
1182    reclamation: Arc<crate::memory::AllocationReclamation>,
1183}
1184
1185impl fmt::Debug for RetainedAllocation {
1186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1187        f.debug_struct("RetainedAllocation")
1188            .field("bytes", &self.bytes)
1189            .field("cause", &self.cause)
1190            .field("released", &self.reclamation.was_released())
1191            .finish_non_exhaustive()
1192    }
1193}
1194
1195impl ResourceError {
1196    pub(crate) fn retaining(
1197        self,
1198        bytes: usize,
1199        reclamation: Arc<crate::memory::AllocationReclamation>,
1200    ) -> Self {
1201        Self::AllocationRetained(RetainedAllocation {
1202            bytes,
1203            cause: Box::new(self),
1204            reclamation,
1205        })
1206    }
1207
1208    #[cfg(test)]
1209    pub(crate) fn retained_allocation_bytes(&self) -> usize {
1210        match self {
1211            Self::AllocationRetained(owner) => owner.bytes,
1212            _ => 0,
1213        }
1214    }
1215}
1216
1217pub type ResourceResult<T> = std::result::Result<T, ResourceError>;
1218
1219impl From<cudarc::driver::DriverError> for ResourceError {
1220    fn from(error: cudarc::driver::DriverError) -> Self {
1221        Self::Driver(error.to_string())
1222    }
1223}
1224
1225/// One immutable block identity and its strongest access for a launch.
1226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1227pub(crate) struct BlockUse {
1228    pub(crate) block: BlockId,
1229    /// Complete allocation extent captured from the actual block owner.
1230    pub(crate) bytes: usize,
1231    pub(crate) access: Access,
1232}
1233
1234/// A checked half-open device byte range and the operation's access to it.
1235/// Allocation generations and import wrapper identities do not distinguish
1236/// overlapping storage in one CUDA context.
1237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1238pub(crate) struct MemoryUse {
1239    start: u64,
1240    end: u64,
1241    access: Access,
1242}
1243
1244impl MemoryUse {
1245    pub(crate) fn access(self) -> Access {
1246        self.access
1247    }
1248
1249    pub(crate) fn new(start: u64, bytes: usize, access: Access) -> ResourceResult<Self> {
1250        let bytes = u64::try_from(bytes)
1251            .map_err(|_| ResourceError::Driver("device byte range is not representable".into()))?;
1252        let end = start.checked_add(bytes).ok_or_else(|| {
1253            ResourceError::Driver("device byte range overflows its address".into())
1254        })?;
1255        Ok(Self { start, end, access })
1256    }
1257
1258    pub(crate) fn covers(self, other: Self) -> bool {
1259        self.start <= other.start
1260            && self.end >= other.end
1261            && (!other.access.reads() || self.access.reads())
1262            && (!other.access.writes() || self.access.writes())
1263    }
1264
1265    pub(crate) fn validate_copy_to(
1266        self,
1267        source_context: usize,
1268        destination: Self,
1269        destination_context: usize,
1270    ) -> ResourceResult<()> {
1271        if self.end - self.start != destination.end - destination.start {
1272            return Err(ResourceError::StreamMisuse(
1273                "device copy source and destination extents differ".into(),
1274            ));
1275        }
1276        if source_context == destination_context && self.overlaps(destination) {
1277            return Err(ResourceError::StreamMisuse(
1278                "device copy source and destination overlap".into(),
1279            ));
1280        }
1281        Ok(())
1282    }
1283
1284    fn overlaps(self, other: Self) -> bool {
1285        self.start < self.end
1286            && other.start < other.end
1287            && self.start < other.end
1288            && other.start < self.end
1289    }
1290
1291    fn conflicts(self, other: Self) -> bool {
1292        self.overlaps(other) && (self.access.writes() || other.access.writes())
1293    }
1294}
1295
1296/// Implemented by concrete native/runtime/import storage owners. An operation
1297/// retains this owner, not just a pointer, allocation identity, or budget lease.
1298pub(crate) trait MemoryStorageOwner: Send + Sync {
1299    fn dependencies(&self) -> ResourceResult<Arc<DeviceAccessDependencies>>;
1300}
1301
1302pub(crate) struct RetainedStorageUse {
1303    pub(crate) owner: Arc<dyn MemoryStorageOwner>,
1304    pub(crate) access: Access,
1305}
1306
1307struct RegisteredStorage {
1308    range: MemoryUse,
1309    owner: std::sync::Weak<dyn MemoryStorageOwner>,
1310    // Set only after cold retirement has finished. A failed Weak upgrade while
1311    // this is false means retirement is running or uncertain, not empty history.
1312    released: Arc<std::sync::atomic::AtomicBool>,
1313}
1314
1315#[derive(Default)]
1316struct RegisteredStorageIndex {
1317    // The maximum registered span bounds the only start keys that can overlap
1318    // a queried range, while the ordered map excludes later allocations.
1319    by_start: BTreeMap<u64, Vec<RegisteredStorage>>,
1320    max_span: u64,
1321}
1322
1323/// Exact reservation identity. Releasing one operation cannot release a
1324/// different operation with the same ranges, context, or stream.
1325#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1326pub(crate) struct MemoryUseGroup(u64);
1327
1328struct PreparedMemoryUses {
1329    uses: Box<[(usize, MemoryUse)]>,
1330    release_proof: Option<Arc<std::sync::atomic::AtomicBool>>,
1331}
1332
1333impl PreparedMemoryUses {
1334    fn physically_released(&self) -> bool {
1335        self.release_proof
1336            .as_ref()
1337            .is_some_and(|proof| proof.load(Ordering::Acquire))
1338    }
1339}
1340
1341/// Pending ordinary block uses and storage-range reservations. Range
1342/// reservations belong to an operation, not to an imported wrapper. The
1343/// operation's owner capsule retains the actual storage and CUDA context.
1344#[derive(Default)]
1345pub(crate) struct BlockUseRegistry {
1346    prepared_memory: HashMap<MemoryUseGroup, PreparedMemoryUses>,
1347    next_memory_group: u64,
1348    storage: HashMap<usize, RegisteredStorageIndex>,
1349}
1350
1351/// One admission registry for safe XLOG operations, including independently
1352/// constructed runtimes sharing the same CUDA context. It does not own those
1353/// runtimes or their allocations; live operations retain their concrete owners.
1354pub(crate) fn block_use_registry() -> &'static Mutex<BlockUseRegistry> {
1355    static REGISTRY: OnceLock<Mutex<BlockUseRegistry>> = OnceLock::new();
1356    REGISTRY.get_or_init(|| Mutex::new(BlockUseRegistry::default()))
1357}
1358
1359impl BlockUseRegistry {
1360    pub(crate) fn register_storage(
1361        &mut self,
1362        context: usize,
1363        range: MemoryUse,
1364        owner: std::sync::Weak<dyn MemoryStorageOwner>,
1365        released: Arc<std::sync::atomic::AtomicBool>,
1366    ) {
1367        let index = self.storage.entry(context).or_default();
1368        index.max_span = index.max_span.max(range.end.saturating_sub(range.start));
1369        let entries = index.by_start.entry(range.start).or_default();
1370        entries.retain(|entry| !entry.released.load(std::sync::atomic::Ordering::Acquire));
1371        entries.push(RegisteredStorage {
1372            range,
1373            owner,
1374            released,
1375        });
1376    }
1377
1378    /// Append strong owner snapshots to caller-owned storage. On error this
1379    /// deliberately leaves earlier snapshots in the caller's vector, so the
1380    /// caller can release the registry mutex before dropping the last owner.
1381    pub(crate) fn retain_storage_uses(
1382        &mut self,
1383        context: usize,
1384        uses: &[MemoryUse],
1385        retained: &mut Vec<RetainedStorageUse>,
1386    ) -> ResourceResult<()> {
1387        let Some(index) = self.storage.get_mut(&context) else {
1388            return Ok(());
1389        };
1390        // A resident graph may admit thousands of spans for each captured
1391        // region. Preserve the first matching owner and its access merge
1392        // semantics without rescanning the growing retained list per span.
1393        // Arc::ptr_eq compares the allocation data pointer, not trait metadata.
1394        let mut retained_by_owner = HashMap::with_capacity(retained.len());
1395        for (position, existing) in retained.iter().enumerate() {
1396            retained_by_owner
1397                .entry(Arc::as_ptr(&existing.owner) as *const ())
1398                .or_insert(position);
1399        }
1400        for use_ in uses {
1401            let first_possible_start = use_.start.saturating_sub(index.max_span);
1402            for entries in index
1403                .by_start
1404                .range_mut(first_possible_start..use_.end)
1405                .map(|(_, entries)| entries)
1406            {
1407                entries.retain(|entry| !entry.released.load(std::sync::atomic::Ordering::Acquire));
1408                for entry in entries {
1409                    if !use_.overlaps(entry.range) {
1410                        continue;
1411                    }
1412                    let owner = entry.owner.upgrade().ok_or_else(|| {
1413                        ResourceError::StreamMisuse(
1414                            "overlapping device storage is retiring without proven release".into(),
1415                        )
1416                    })?;
1417                    let owner_key = Arc::as_ptr(&owner) as *const ();
1418                    if let Some(&position) = retained_by_owner.get(&owner_key) {
1419                        let existing = &mut retained[position];
1420                        existing.access = if existing.access.writes() || use_.access.writes() {
1421                            Access::ReadWrite
1422                        } else {
1423                            Access::Read
1424                        };
1425                    } else {
1426                        retained_by_owner.insert(owner_key, retained.len());
1427                        retained.push(RetainedStorageUse {
1428                            owner,
1429                            access: use_.access,
1430                        });
1431                    }
1432                }
1433            }
1434        }
1435        Ok(())
1436    }
1437
1438    /// Admit a complete operation atomically. Aliases within the operation are
1439    /// deliberately compared only against other operations: kernels may read
1440    /// and write the same storage. Keeping exact spans also preserves read-only
1441    /// subranges and gaps instead of making a bounding interval exclusive.
1442    #[cfg(test)]
1443    pub(crate) fn reserve_memory_uses(
1444        &mut self,
1445        context: usize,
1446        uses: &[MemoryUse],
1447    ) -> ResourceResult<MemoryUseGroup> {
1448        self.reserve_context_memory_uses(
1449            &uses.iter().map(|use_| (context, *use_)).collect::<Vec<_>>(),
1450        )
1451    }
1452
1453    /// A peer copy reserves source and destination contexts in the same atomic
1454    /// group. No partial source reservation remains if the destination conflicts.
1455    pub(crate) fn reserve_context_memory_uses(
1456        &mut self,
1457        uses: &[(usize, MemoryUse)],
1458    ) -> ResourceResult<MemoryUseGroup> {
1459        self.prepared_memory
1460            .retain(|_, pending| !pending.physically_released());
1461        for pending in self.prepared_memory.values() {
1462            if pending.uses.iter().any(|(prior_context, prior)| {
1463                uses.iter().any(|(next_context, next)| {
1464                    prior_context == next_context && prior.conflicts(*next)
1465                })
1466            }) {
1467                return Err(ResourceError::StreamMisuse(
1468                    "device storage overlaps a conflicting pending operation".into(),
1469                ));
1470            }
1471        }
1472        let next = self.next_memory_group.checked_add(1).ok_or_else(|| {
1473            ResourceError::Driver("device memory-use reservation identity exhausted".into())
1474        })?;
1475        let group = MemoryUseGroup(next);
1476        let uses = uses.to_vec().into_boxed_slice();
1477        self.prepared_memory.insert(
1478            group,
1479            PreparedMemoryUses {
1480                uses,
1481                release_proof: None,
1482            },
1483        );
1484        self.next_memory_group = next;
1485        Ok(group)
1486    }
1487
1488    /// The freeing operation stays exclusive until the actual raw owner proves
1489    /// release, including when another reaper completes it after an error.
1490    pub(crate) fn reserve_reclamation(
1491        &mut self,
1492        context: usize,
1493        memory: MemoryUse,
1494        release_proof: Arc<std::sync::atomic::AtomicBool>,
1495    ) -> ResourceResult<MemoryUseGroup> {
1496        let group = self.reserve_context_memory_uses(&[(context, memory)])?;
1497        self.prepared_memory
1498            .get_mut(&group)
1499            .expect("new reservation present")
1500            .release_proof = Some(release_proof);
1501        Ok(group)
1502    }
1503
1504    /// Reserve before checking the source proofs: a concurrent late reaper can
1505    /// publish release without this mutex. Its old free group must either still
1506    /// conflict, or be pruned before we reject the now-released source owner.
1507    pub(crate) fn reserve_owned_memory_uses(
1508        &mut self,
1509        uses: &[(usize, MemoryUse)],
1510        source_proofs: &[Arc<std::sync::atomic::AtomicBool>],
1511    ) -> ResourceResult<MemoryUseGroup> {
1512        let group = self.reserve_context_memory_uses(uses)?;
1513        if source_proofs
1514            .iter()
1515            .any(|proof| proof.load(Ordering::Acquire))
1516        {
1517            self.release_memory_uses(group)?;
1518            return Err(ResourceError::StreamMisuse(
1519                "device memory source owner has already been physically released".into(),
1520            ));
1521        }
1522        Ok(group)
1523    }
1524
1525    /// Cancel before enqueue, or release after completion has been proved.
1526    /// Event publication alone is not completion; the enclosing transaction
1527    /// must retain this reservation if enqueue or synchronization is uncertain.
1528    pub(crate) fn release_memory_uses(&mut self, group: MemoryUseGroup) -> ResourceResult<()> {
1529        self.prepared_memory
1530            .remove(&group)
1531            .map(|_| ())
1532            .ok_or_else(|| {
1533                ResourceError::StreamMisuse("memory-use release has no matching reservation".into())
1534            })
1535    }
1536}
1537
1538/// Exact byte-accounting snapshot exposed by a reservable resource decorator.
1539#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1540pub struct ResourceBudgetSnapshot {
1541    pub limit: usize,
1542    pub reserved: usize,
1543}
1544
1545impl ResourceBudgetSnapshot {
1546    pub fn remaining(self) -> usize {
1547        self.limit.saturating_sub(self.reserved)
1548    }
1549}
1550
1551/// Physical retirement starts only after the last shared allocation lease has
1552/// gone. The exact reservation survives an error or unwind and is retired by
1553/// the same release proof as the allocation and its accounting.
1554pub(crate) fn with_reclamation_admission(
1555    registry: &Mutex<BlockUseRegistry>,
1556    context: usize,
1557    memory: MemoryUse,
1558    released: Arc<std::sync::atomic::AtomicBool>,
1559    admission: &mut Option<MemoryUseGroup>,
1560    reclaim: impl FnOnce() -> ResourceResult<()>,
1561) -> ResourceResult<()> {
1562    if admission.is_none() {
1563        *admission = Some(
1564            registry
1565                .lock()
1566                .expect("device block-use registry poisoned")
1567                .reserve_reclamation(context, memory, released)?,
1568        );
1569    }
1570    // No driver work, owner destruction, or callback runs under the registry.
1571    reclaim()
1572}
1573
1574/// Stream-ordered device memory resource. Implementations:
1575///   * [`crate::device_runtime::direct::DirectCudaResource`] —
1576///     cudarc default (non-pooled) backend; **candidate** for the
1577///     sanitizer/cert role, **unproven** until the manual Compute Sanitizer
1578///     acceptance gate runs on a supported host.
1579///   * [`crate::device_runtime::async_resource::AsyncCudaResource`] —
1580///     stream-ordered cuMemAllocAsync/cuMemFreeAsync backend;
1581///     production default when the context supports async-alloc.
1582///   * [`crate::device_runtime::logging::LoggingResource`] —
1583///     telemetry decorator over any inner resource.
1584///   * [`crate::device_runtime::budget::GlobalDeviceBudget`] —
1585///     per-runtime byte-limit decorator over any inner resource.
1586///
1587/// Implementations must be thread-safe. The runtime composes resources
1588/// via decoration (each resource wraps an inner `Box<dyn
1589/// DeviceMemoryResource + Send + Sync>`).
1590pub trait DeviceMemoryResource: Send + Sync {
1591    /// Materialize one request. Decorators must forward the same request.
1592    ///
1593    /// The terminal backend binds its ledger with `attach_resource` before
1594    /// malloc, retains the ticket with its storage owner, and calls `acquired`
1595    /// immediately after arming the actual pointer. Failed initialization must
1596    /// still reach physical reclamation. Only proven physical release permits
1597    /// `unsafe { ticket.confirm_physical_release() }`; an error or unwind alone
1598    /// never proves storage was released.
1599    ///
1600    /// Custom implementations must implement this lifecycle-bearing entry point;
1601    /// ordinary caller-facing allocation signatures remain conveniences below.
1602    fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock>;
1603
1604    /// Exact terminal-backend accounting domain. Decorators return the identical
1605    /// Arc. Later allocations must pass through the domain's installed budgets.
1606    fn allocation_accounting(&self) -> Arc<AllocationAccounting>;
1607
1608    /// Allocate `bytes` bytes on the resource's device, ordered on
1609    /// `stream`. The returned block is in [`BlockState::Live`].
1610    fn allocate(
1611        &self,
1612        bytes: usize,
1613        stream: StreamId,
1614        tag: AllocTag,
1615    ) -> ResourceResult<DeviceBlock> {
1616        self.materialize(AllocationRequest::new(bytes, stream, tag))
1617    }
1618
1619    /// Allocate while accounting for bytes promised by the runtime but not yet
1620    /// materialized through this resource stack.
1621    ///
1622    /// Budget decorators must include the reservation pressure in their
1623    /// admission decision. Telemetry decorators must forward this call so they
1624    /// observe both successful allocations and admission failures. All layers
1625    /// forward the same lifecycle-bearing request to `materialize`.
1626    fn allocate_with_reservation_pressure(
1627        &self,
1628        bytes: usize,
1629        reservation_pressure_bytes: usize,
1630        stream: StreamId,
1631        tag: AllocTag,
1632    ) -> ResourceResult<DeviceBlock> {
1633        let mut request = AllocationRequest::new(bytes, stream, tag);
1634        request.reservation_pressure_bytes = reservation_pressure_bytes;
1635        self.materialize(request)
1636    }
1637
1638    /// Return `block` to the resource. After this call the block's
1639    /// state is [`BlockState::Retired`] (or [`BlockState::Quarantined`]
1640    /// for debug-guard resources). Reuse of the underlying memory is
1641    /// resource-specific but must respect the stream-ordered contract.
1642    ///
1643    /// `block.alloc_stream` is authoritative for ordering. If the
1644    /// caller has touched the memory on a different stream, they must
1645    /// have synchronized before calling `deallocate`.
1646    fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()>;
1647
1648    /// CUDA device ordinal this resource serves. Resources are pinned
1649    /// to a single device.
1650    fn device_ordinal(&self) -> u32;
1651
1652    /// Bytes currently outstanding (live + retired-but-not-yet-freed).
1653    /// Used by tests and by the global budget adaptor.
1654    fn bytes_outstanding(&self) -> usize;
1655
1656    /// Return the outer resource stack's reservable budget, if one exists.
1657    /// Base allocators have no declarative admission limit and return `None`;
1658    /// decorators must forward this query unless they enforce their own limit.
1659    fn budget_snapshot(&self) -> Option<ResourceBudgetSnapshot> {
1660        None
1661    }
1662
1663    /// Drain any retired-but-not-yet-freed bytes whose underlying
1664    /// CUDA work has completed. For synchronous backends this is a
1665    /// no-op. For stream-ordered async backends this synchronizes
1666    /// the streams that have queued `cuMemFreeAsync` calls and
1667    /// re-counts `bytes_outstanding` accordingly.
1668    ///
1669    /// Callers that need an accurate budget reading after a burst
1670    /// of asynchronous deallocations should call this before
1671    /// reading `bytes_outstanding`. Calling on a synchronous backend
1672    /// is harmless and free.
1673    fn reap_pending(&self) -> ResourceResult<()> {
1674        Ok(())
1675    }
1676
1677    /// Record that work has been (or is being) submitted on
1678    /// `use_stream` that touches `block`'s bytes. Resources that
1679    /// participate in cross-stream lifetime tracking (notably the
1680    /// stream-ordered async backend) MUST attach a CUDA event from
1681    /// `use_stream` to the block; on `deallocate(block)`, the
1682    /// block's `alloc_stream` will wait on every recorded event
1683    /// before queueing the underlying free.
1684    ///
1685    /// **The default implementation returns
1686    /// [`ResourceError::StreamMisuse`].** This is intentional: a
1687    /// silent no-op default would let a launch builder call
1688    /// `record_block_use` against a resource that does not
1689    /// actually track cross-stream uses (e.g.,
1690    /// [`crate::device_runtime::direct::DirectCudaResource`]),
1691    /// observe `Ok(())`, queue a kernel on a different stream,
1692    /// then drop the block — and quietly hit the cross-stream
1693    /// use-after-free that this API exists to prevent. False
1694    /// safety is worse than no safety. Resources that cannot
1695    /// track cross-stream uses MUST inherit this default;
1696    /// callers (notably the future xlog launch builder) MUST
1697    /// surface the error rather than masking it.
1698    ///
1699    /// Override status today:
1700    ///   * [`crate::device_runtime::async_resource::AsyncCudaResource`]
1701    ///     overrides with real event tracking.
1702    ///   * [`crate::device_runtime::logging::LoggingResource`] and
1703    ///     [`crate::device_runtime::budget::GlobalDeviceBudget`]
1704    ///     forward to their inner resource (so the underlying
1705    ///     backend's behavior surfaces unchanged).
1706    ///   * [`crate::device_runtime::direct::DirectCudaResource`]
1707    ///     does NOT override — it correctly returns
1708    ///     `StreamMisuse` and forces callers to either route
1709    ///     allocations through `AsyncCudaResource` or take
1710    ///     responsibility for cross-stream synchronization
1711    ///     themselves.
1712    ///
1713    /// # Errors
1714    ///   * [`ResourceError::StreamMisuse`] from the default impl
1715    ///     when the resource cannot track cross-stream uses.
1716    ///   * [`ResourceError::UseAfterFree`] if `block` is not the
1717    ///     block currently live at `block.ptr` (caller likely
1718    ///     handed back a stale [`DeviceBlock`] whose generation
1719    ///     no longer matches the live entry).
1720    ///   * [`ResourceError::StreamMisuse`] if `use_stream` does
1721    ///     not resolve in the resource's stream pool.
1722    ///   * [`ResourceError::Driver`] for CUDA driver / event
1723    ///     creation failures.
1724    ///
1725    /// Callers that bypass this API and submit cross-stream work
1726    /// directly (raw `cuMemcpyDtoHAsync`, raw `Vec<*mut c_void>`
1727    /// kernel launches that the launch builder did not see, etc.)
1728    /// are responsible for their own cross-stream synchronization.
1729    /// The resource cannot infer arbitrary external CUDA work.
1730    fn record_block_use(&self, block: &DeviceBlock, use_stream: StreamId) -> ResourceResult<()> {
1731        let _ = (block, use_stream);
1732        Err(ResourceError::StreamMisuse(
1733            "record_block_use unsupported by this resource (the active backend \
1734             does not track cross-stream uses; route allocations through a \
1735             stream-ordered backend such as AsyncCudaResource, or take \
1736             responsibility for cross-stream synchronization explicitly)"
1737                .to_string(),
1738        ))
1739    }
1740
1741    /// Whether this resource (and any inner resources it
1742    /// composes) actually tracks cross-stream uses via
1743    /// `record_block_use`. Used by the launch recorder's
1744    /// preflight to fail BEFORE queueing CUDA work, rather than
1745    /// after. The default returns `false` to match the trait's
1746    /// default `record_block_use` behavior; resources that
1747    /// override `record_block_use` to track events MUST override
1748    /// this to return `true`. Decorators forward to inner.
1749    fn supports_block_use_tracking(&self) -> bool {
1750        false
1751    }
1752
1753    /// Share the allocation's existing access history with its actual storage
1754    /// owner. Decorators forward the same Arc; reconstructing event history
1755    /// would lose writes already published through this resource.
1756    ///
1757    /// A non-tracking allocator has no history to share. A tracking allocator
1758    /// must implement this method instead of claiming that its history is empty.
1759    #[doc(hidden)]
1760    fn access_dependencies(
1761        &self,
1762        _block: BlockId,
1763        _bytes: usize,
1764    ) -> ResourceResult<Option<Arc<DeviceAccessDependencies>>> {
1765        if self.supports_block_use_tracking() {
1766            Err(ResourceError::StreamMisuse(
1767                "tracking allocator does not expose its allocation dependency owner".into(),
1768            ))
1769        } else {
1770            Ok(None)
1771        }
1772    }
1773
1774    /// Pre-launch / pre-copy hook: queue any cross-stream waits
1775    /// required for `use_stream` to safely access `block` with
1776    /// `access` semantics. MUST be called BEFORE the GPU work is
1777    /// enqueued on `use_stream`.
1778    ///
1779    /// On [`Access::Read`] the resource must queue waits on every
1780    /// outstanding write from a different stream. On [`Access::Write`] /
1781    /// [`Access::ReadWrite`] the resource must additionally queue
1782    /// waits on every outstanding read recorded on a different
1783    /// stream — the writer must observe completion of every prior
1784    /// reader. Same-stream events are skipped (CUDA stream order
1785    /// already covers them).
1786    ///
1787    /// **The default implementation returns
1788    /// [`ResourceError::StreamMisuse`].** Same rationale as
1789    /// `record_block_use`: a silent no-op default would let
1790    /// callers paired against a non-tracking backend believe the
1791    /// dependency edge was queued. Decorators forward; tracking
1792    /// backends override.
1793    ///
1794    /// # Errors
1795    ///   * [`ResourceError::StreamMisuse`] from the default impl
1796    ///     when the resource cannot track cross-stream uses.
1797    ///   * [`ResourceError::UseAfterFree`] if `block` is not the
1798    ///     id currently live at `block.ptr`.
1799    ///   * [`ResourceError::Driver`] for CUDA driver / event-wait
1800    ///     failures.
1801    fn prepare_block_use(
1802        &self,
1803        block: BlockId,
1804        use_stream: StreamId,
1805        access: Access,
1806    ) -> ResourceResult<()> {
1807        let _ = (block, use_stream, access);
1808        Err(ResourceError::StreamMisuse(
1809            "prepare_block_use unsupported by this resource (the active backend \
1810             does not track cross-stream uses; route allocations through \
1811             AsyncCudaResource or take responsibility for cross-stream \
1812             synchronization explicitly)"
1813                .to_string(),
1814        ))
1815    }
1816
1817    /// Post-launch / post-copy hook: record an event on
1818    /// `use_stream` capturing the work just enqueued and update
1819    /// `block`'s dependency state.
1820    ///
1821    /// Advance the recording stream's read or write frontier. Publication must
1822    /// preserve other streams' concurrent completions: disjoint ranges of one
1823    /// allocation can prepare before either operation publishes. Only events
1824    /// whose ordering is proved may be retired. Same-stream record and frontier
1825    /// replacement must be serialized so an older host publisher cannot erase
1826    /// a newer completion.
1827    ///
1828    /// **The default implementation returns
1829    /// [`ResourceError::StreamMisuse`].** Same rationale as
1830    /// `record_block_use`. Decorators forward; tracking backends
1831    /// override.
1832    fn finish_block_use(
1833        &self,
1834        block: BlockId,
1835        use_stream: StreamId,
1836        access: Access,
1837    ) -> ResourceResult<()> {
1838        let _ = (block, use_stream, access);
1839        Err(ResourceError::StreamMisuse(
1840            "finish_block_use unsupported by this resource (the active backend \
1841             does not track cross-stream uses; route allocations through \
1842             AsyncCudaResource or take responsibility for cross-stream \
1843             synchronization explicitly)"
1844                .to_string(),
1845        ))
1846    }
1847}