1use std::collections::HashMap;
96use std::sync::Arc;
97
98use crate::device_runtime::{
99 Access, BlockId, DeviceBlock, Generation, ResourceError, ResourceResult, StreamId,
100 XlogDeviceRuntime,
101};
102
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
105pub enum RecorderMode {
106 Permissive,
110 Strict,
113}
114
115pub struct LaunchRecorder {
143 launch_stream: StreamId,
144 mode: RecorderMode,
145 uses: Vec<RecordedUse>,
149 strict_reject: Option<ResourceError>,
153 preflighted: bool,
157 committed: bool,
158 bound_runtime: Option<Arc<XlogDeviceRuntime>>,
159 bound_domain: Option<Arc<()>>,
160}
161
162#[derive(Clone, Copy)]
163struct RecordedUse {
164 block: BlockId,
165 access: Access,
166 #[allow(dead_code)]
170 label: &'static str,
171}
172
173impl LaunchRecorder {
174 pub fn new_permissive(launch_stream: StreamId) -> Self {
176 Self::new(launch_stream, RecorderMode::Permissive)
177 }
178
179 pub fn new_strict(launch_stream: StreamId) -> Self {
182 Self::new(launch_stream, RecorderMode::Strict)
183 }
184
185 pub(crate) fn new_strict_bound(
186 launch_stream: StreamId,
187 runtime: Arc<XlogDeviceRuntime>,
188 domain: Arc<()>,
189 ) -> Self {
190 let mut recorder = Self::new(launch_stream, RecorderMode::Strict);
191 recorder.bound_runtime = Some(runtime);
192 recorder.bound_domain = Some(domain);
193 recorder
194 }
195
196 fn new(launch_stream: StreamId, mode: RecorderMode) -> Self {
197 Self {
198 launch_stream,
199 mode,
200 uses: Vec::new(),
201 strict_reject: None,
202 preflighted: false,
203 committed: false,
204 bound_runtime: None,
205 bound_domain: None,
206 }
207 }
208
209 pub fn launch_stream(&self) -> StreamId {
211 self.launch_stream
212 }
213
214 pub fn mode(&self) -> RecorderMode {
216 self.mode
217 }
218
219 pub(crate) fn require_bound_domain<'a>(
220 &'a mut self,
221 runtime: &Arc<XlogDeviceRuntime>,
222 domain: &Arc<()>,
223 launch_stream: StreamId,
224 ) -> &'a mut Self {
225 let matches = self.mode == RecorderMode::Strict
226 && self.launch_stream == launch_stream
227 && same_arc_identity(self.bound_runtime.as_ref(), runtime)
228 && same_arc_identity(self.bound_domain.as_ref(), domain);
229 if !matches && self.strict_reject.is_none() {
230 self.strict_reject = Some(ResourceError::StreamMisuse(
231 "LaunchRecorder: recorder is not bound to the resident execution domain"
232 .to_string(),
233 ));
234 }
235 self
236 }
237
238 pub(crate) fn preflight_bound(
239 &mut self,
240 runtime: &Arc<XlogDeviceRuntime>,
241 ) -> ResourceResult<()> {
242 if !same_arc_identity(self.bound_runtime.as_ref(), runtime) || self.bound_domain.is_none() {
243 return Err(ResourceError::StreamMisuse(
244 "LaunchRecorder::preflight_bound: foreign or unbound runtime".to_string(),
245 ));
246 }
247 self.preflight(runtime.as_ref())
248 }
249
250 fn note(
254 &mut self,
255 label: &'static str,
256 block: Option<&DeviceBlock>,
257 access: Access,
258 external: bool,
259 ) -> &mut Self {
260 self.note_identity(label, block.map(BlockId::from_block), access, external)
261 }
262
263 fn note_identity(
264 &mut self,
265 label: &'static str,
266 block: Option<BlockId>,
267 access: Access,
268 external: bool,
269 ) -> &mut Self {
270 if self.preflighted && self.strict_reject.is_none() {
271 self.strict_reject = Some(ResourceError::StreamMisuse(format!(
272 "LaunchRecorder::{}: recorded after preflight — once preflight \
273 succeeds, the set of uses is frozen so commit-time discoveries \
274 cannot leave unprotected work in flight. Record this use BEFORE \
275 preflight (the recorder is lifetime-free; snapshots release the \
276 source borrow immediately, so kernel-param &mut borrows still \
277 work)",
278 label,
279 )));
280 return self;
281 }
282 if let Some(b) = block {
283 self.uses.push(RecordedUse {
284 block: b,
285 access,
286 label,
287 });
288 return self;
289 }
290 if self.mode == RecorderMode::Strict && self.strict_reject.is_none() {
291 let why = if external {
292 "external (DLPack / ArrowDevice) memory has no runtime identity; \
293 strict launch recorders cannot attach a cross-stream use to it. \
294 Use a permissive recorder OR coordinate the cross-stream \
295 synchronization explicitly outside xlog"
296 } else {
297 "buffer is legacy cudarc-backed (no runtime block); strict launch \
298 recorders require the allocation to be routed through \
299 GpuMemoryManager::with_runtime so a DeviceBlock is available"
300 };
301 self.strict_reject = Some(ResourceError::StreamMisuse(format!(
302 "LaunchRecorder::{}: untracked buffer rejected — {}",
303 label, why
304 )));
305 }
306 self
307 }
308
309 pub fn read<T: cudarc::driver::DeviceRepr>(
312 &mut self,
313 slice: &crate::memory::TrackedCudaSlice<T>,
314 ) -> &mut Self {
315 self.note("read", slice.runtime_block(), Access::Read, false)
316 }
317
318 pub(crate) fn read_device_block(&mut self, block: &DeviceBlock) -> &mut Self {
324 self.note("read_device_block", Some(block), Access::Read, false)
325 }
326
327 pub(crate) fn read_block_identity(&mut self, block: BlockId) -> &mut Self {
329 self.note_identity("read_block_identity", Some(block), Access::Read, false)
330 }
331
332 pub(crate) fn read_optional_block_identity(&mut self, block: Option<BlockId>) -> &mut Self {
333 self.note_identity("read_optional_block_identity", block, Access::Read, false)
334 }
335
336 pub fn write<T: cudarc::driver::DeviceRepr>(
343 &mut self,
344 slice: &crate::memory::TrackedCudaSlice<T>,
345 ) -> &mut Self {
346 self.note("write", slice.runtime_block(), Access::Write, false)
347 }
348
349 pub fn read_write<T: cudarc::driver::DeviceRepr>(
352 &mut self,
353 slice: &crate::memory::TrackedCudaSlice<T>,
354 ) -> &mut Self {
355 self.note(
356 "read_write",
357 slice.runtime_block(),
358 Access::ReadWrite,
359 false,
360 )
361 }
362
363 pub fn read_column(&mut self, col: &crate::memory::CudaColumn) -> &mut Self {
368 self.note(
369 "read_column",
370 col.runtime_block(),
371 Access::Read,
372 col.is_external(),
373 )
374 }
375
376 pub fn write_column(&mut self, col: &crate::memory::CudaColumn) -> &mut Self {
379 self.note(
380 "write_column",
381 col.runtime_block(),
382 Access::Write,
383 col.is_external(),
384 )
385 }
386
387 pub fn recorded_count(&self) -> usize {
389 self.uses.len()
390 }
391
392 pub fn preflight(&mut self, runtime: &XlogDeviceRuntime) -> ResourceResult<()> {
421 if let Some(bound_runtime) = &self.bound_runtime {
422 if !std::ptr::eq(bound_runtime.as_ref(), runtime) {
423 return Err(ResourceError::StreamMisuse(
424 "LaunchRecorder::preflight: bound recorder received a foreign runtime"
425 .to_string(),
426 ));
427 }
428 }
429 if let Some(err) = &self.strict_reject {
430 return Err(ResourceError::StreamMisuse(format!("{}", err)));
433 }
434 if !self.uses.is_empty() && !runtime.supports_block_use_tracking() {
435 return Err(ResourceError::StreamMisuse(
436 "LaunchRecorder::preflight: active resource does not support \
437 cross-stream use tracking. Build the runtime around \
438 AsyncCudaResource (or a decorator stack over it) for \
439 stream-lifetime-safe launches"
440 .to_string(),
441 ));
442 }
443
444 let deduped = dedup_uses(&self.uses);
445 for use_ in &deduped {
446 runtime.prepare_block_use(use_.block, self.launch_stream, use_.access)?;
447 }
448
449 self.preflighted = true;
450 Ok(())
451 }
452
453 pub fn commit(self, runtime: &XlogDeviceRuntime) -> ResourceResult<()> {
477 if self.bound_runtime.is_some() || self.bound_domain.is_some() {
478 return Err(ResourceError::StreamMisuse(
479 "LaunchRecorder::commit: domain-bound recorder requires commit_bound".to_string(),
480 ));
481 }
482 self.commit_inner(runtime)
483 }
484
485 pub(crate) fn commit_bound(
486 self,
487 runtime: &Arc<XlogDeviceRuntime>,
488 domain: &Arc<()>,
489 ) -> ResourceResult<()> {
490 if !same_arc_identity(self.bound_runtime.as_ref(), runtime)
491 || !same_arc_identity(self.bound_domain.as_ref(), domain)
492 {
493 return Err(ResourceError::StreamMisuse(
494 "LaunchRecorder::commit_bound: foreign or unbound execution domain".to_string(),
495 ));
496 }
497 self.commit_inner(runtime.as_ref())
498 }
499
500 fn commit_inner(mut self, runtime: &XlogDeviceRuntime) -> ResourceResult<()> {
501 if let Some(err) = self.strict_reject.take() {
506 return Err(err);
507 }
508 if !self.uses.is_empty() && !self.preflighted {
509 return Err(ResourceError::StreamMisuse(
510 "LaunchRecorder::commit: non-empty recorder reached commit without \
511 a successful preflight. The caller MUST call preflight(&runtime) \
512 BEFORE enqueueing CUDA work; otherwise commit-time failures leave \
513 unprotected work in flight. See the preflight + commit contract \
514 in the LaunchRecorder doc"
515 .to_string(),
516 ));
517 }
518
519 let deduped = dedup_uses(&self.uses);
520 for use_ in &deduped {
521 runtime.finish_block_use(use_.block, self.launch_stream, use_.access)?;
522 }
523 self.committed = true;
524 Ok(())
525 }
526}
527
528fn dedup_uses(uses: &[RecordedUse]) -> Vec<RecordedUse> {
545 let mut by_id: HashMap<(u64, Generation, StreamId, u32), usize> =
546 HashMap::with_capacity(uses.len());
547 let mut deduped: Vec<RecordedUse> = Vec::with_capacity(uses.len());
548 for use_ in uses {
549 let key = (
550 use_.block.ptr,
551 use_.block.generation,
552 use_.block.alloc_stream,
553 use_.block.device_ordinal,
554 );
555 match by_id.get(&key) {
556 Some(&idx) => {
557 deduped[idx].access = combine_access(deduped[idx].access, use_.access);
558 }
559 None => {
560 by_id.insert(key, deduped.len());
561 deduped.push(*use_);
562 }
563 }
564 }
565 deduped
566}
567
568fn combine_access(a: Access, b: Access) -> Access {
570 match (a, b) {
571 (Access::ReadWrite, _) | (_, Access::ReadWrite) => Access::ReadWrite,
572 (Access::Read, Access::Write) | (Access::Write, Access::Read) => Access::ReadWrite,
573 (Access::Read, Access::Read) => Access::Read,
574 (Access::Write, Access::Write) => Access::Write,
575 }
576}
577
578fn same_arc_identity<T>(bound: Option<&Arc<T>>, expected: &Arc<T>) -> bool {
579 bound.is_some_and(|bound| Arc::ptr_eq(bound, expected))
580}
581
582impl Drop for LaunchRecorder {
583 fn drop(&mut self) {
584 if !self.committed && !self.uses.is_empty() {
585 #[cfg(debug_assertions)]
586 eprintln!(
587 "[xlog_cuda::launch] LaunchRecorder dropped without commit: \
588 {} uses on launch_stream={} (mode={:?}) were NOT recorded; \
589 cross-stream lifetime safety lost for this launch",
590 self.uses.len(),
591 self.launch_stream.0,
592 self.mode,
593 );
594 }
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use crate::device_runtime::{
602 AsyncCudaResource, DeviceMemoryResource, DirectCudaResource, StreamPool,
603 };
604 use crate::CudaDevice;
605 use std::sync::Arc;
606 use xlog_core::MemoryBudget;
607
608 fn try_async_runtime() -> Option<(Arc<CudaDevice>, Arc<XlogDeviceRuntime>, StreamId)> {
609 let device = Arc::new(CudaDevice::new(0).ok()?);
610 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
611 let async_resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
612 AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
613 );
614 let runtime = Arc::new(XlogDeviceRuntime::with_resource(
615 Arc::clone(&device),
616 0,
617 Arc::clone(&pool),
618 async_resource,
619 ));
620 let launch_stream = pool.acquire().ok()?;
621 Some((device, runtime, launch_stream))
622 }
623
624 fn try_direct_runtime() -> Option<(Arc<CudaDevice>, Arc<XlogDeviceRuntime>, StreamId)> {
625 let device = Arc::new(CudaDevice::new(0).ok()?);
626 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
627 let direct: Box<dyn DeviceMemoryResource + Send + Sync> =
628 Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
629 let runtime = Arc::new(XlogDeviceRuntime::with_resource(
630 Arc::clone(&device),
631 0,
632 Arc::clone(&pool),
633 direct,
634 ));
635 Some((device, runtime, StreamId::DEFAULT))
636 }
637
638 #[test]
639 fn empty_commit_is_ok_in_both_modes() {
640 let Some((_d, rt, ls)) = try_async_runtime() else {
641 return;
642 };
643 LaunchRecorder::new_permissive(ls)
644 .commit(&rt)
645 .expect("permissive empty");
646 LaunchRecorder::new_strict(ls)
647 .commit(&rt)
648 .expect("strict empty");
649 }
650
651 #[test]
652 fn permissive_skips_legacy_silently() {
653 let Some(device) = CudaDevice::new(0).ok().map(Arc::new) else {
654 return;
655 };
656 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
657 let async_resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
658 AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
659 );
660 let runtime = Arc::new(XlogDeviceRuntime::with_resource(
661 Arc::clone(&device),
662 0,
663 Arc::clone(&pool),
664 async_resource,
665 ));
666 let launch_stream = pool.acquire().expect("acquire");
667
668 let manager = Arc::new(crate::GpuMemoryManager::new(
670 Arc::clone(&device),
671 MemoryBudget::with_limit(1024 * 1024),
672 ));
673 let legacy = manager.alloc::<u8>(64).expect("legacy alloc");
674 assert!(legacy.runtime_block().is_none());
675
676 let mut rec = LaunchRecorder::new_permissive(launch_stream);
677 rec.read(&legacy);
678 assert_eq!(rec.recorded_count(), 0);
679 rec.preflight(&runtime).expect("permissive preflight");
680 rec.commit(&runtime).expect("permissive commit");
681 }
682
683 #[test]
684 fn strict_rejects_legacy_at_preflight() {
685 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
686 return;
687 };
688 let manager = Arc::new(crate::GpuMemoryManager::new(
689 Arc::clone(&device),
690 MemoryBudget::with_limit(1024 * 1024),
691 ));
692 let legacy = manager.alloc::<u8>(64).expect("legacy alloc");
693
694 let mut rec = LaunchRecorder::new_strict(launch_stream);
695 rec.read(&legacy);
696 let err = rec.preflight(&runtime);
697 match err {
698 Err(ResourceError::StreamMisuse(msg)) => {
699 assert!(msg.contains("untracked buffer rejected"), "msg: {}", msg);
700 }
701 other => panic!(
702 "strict mode must reject untracked buffer at preflight; got {:?}",
703 other
704 ),
705 }
706 }
707
708 #[test]
709 fn preflight_rejects_direct_runtime_before_enqueue() {
710 let Some((device, runtime, launch_stream)) = try_direct_runtime() else {
711 return;
712 };
713 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
714 Arc::clone(&device),
715 MemoryBudget::with_limit(1024 * 1024),
716 Arc::clone(&runtime),
717 ));
718 let buf = manager.alloc::<u8>(64).expect("alloc");
719 assert!(buf.runtime_block().is_some());
720
721 let mut rec = LaunchRecorder::new_strict(launch_stream);
722 rec.read(&buf);
723 let err = rec.preflight(&runtime);
724 match err {
725 Err(ResourceError::StreamMisuse(msg)) => {
726 assert!(
727 msg.contains("does not support cross-stream use tracking"),
728 "msg: {}",
729 msg
730 );
731 }
732 other => panic!(
733 "preflight must reject Direct-backed runtime before enqueue; got {:?}",
734 other
735 ),
736 }
737 }
738
739 #[test]
740 fn preflight_then_commit_async_runtime() {
741 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
742 return;
743 };
744 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
745 Arc::clone(&device),
746 MemoryBudget::with_limit(1024 * 1024),
747 Arc::clone(&runtime),
748 ));
749 let buf = manager.alloc::<u8>(64).expect("alloc");
750
751 let mut rec = LaunchRecorder::new_strict(launch_stream);
752 rec.read(&buf);
753 rec.preflight(&runtime).expect("preflight ok");
754 rec.commit(&runtime).expect("commit ok");
756 }
757
758 #[test]
759 fn commit_rejects_un_preflighted_strict_recorder() {
760 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
761 return;
762 };
763 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
764 Arc::clone(&device),
765 MemoryBudget::with_limit(1024 * 1024),
766 Arc::clone(&runtime),
767 ));
768 let buf = manager.alloc::<u8>(64).expect("alloc");
769
770 let mut rec = LaunchRecorder::new_strict(launch_stream);
771 rec.read(&buf);
772 let err = rec.commit(&runtime);
773 match err {
774 Err(ResourceError::StreamMisuse(msg)) => {
775 assert!(
776 msg.contains("without a successful preflight"),
777 "msg: {}",
778 msg
779 );
780 }
781 other => panic!(
782 "non-empty un-preflighted commit must return StreamMisuse, got {:?}",
783 other
784 ),
785 }
786 }
787
788 #[test]
789 fn empty_recorder_commit_without_preflight_is_ok() {
790 let Some((_d, rt, ls)) = try_async_runtime() else {
791 return;
792 };
793 LaunchRecorder::new_strict(ls)
794 .commit(&rt)
795 .expect("empty strict commit without preflight");
796 }
797
798 #[test]
799 fn note_after_preflight_via_standard_method_is_rejected() {
800 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
801 return;
802 };
803 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
804 Arc::clone(&device),
805 MemoryBudget::with_limit(1024 * 1024),
806 Arc::clone(&runtime),
807 ));
808 let buf_a = manager.alloc::<u8>(64).expect("alloc a");
809 let buf_b = manager.alloc::<u8>(64).expect("alloc b");
810
811 let mut rec = LaunchRecorder::new_strict(launch_stream);
812 rec.read(&buf_a);
813 rec.preflight(&runtime).expect("preflight ok");
814 rec.read(&buf_b);
815 let err = rec.commit(&runtime);
816 match err {
817 Err(ResourceError::StreamMisuse(msg)) => {
818 assert!(msg.contains("recorded after preflight"), "msg: {}", msg);
819 }
820 other => panic!(
821 "post-preflight standard-method record must be rejected; got {:?}",
822 other
823 ),
824 }
825 }
826
827 #[test]
832 fn pre_preflight_fresh_write_is_accepted() {
833 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
834 return;
835 };
836 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
837 Arc::clone(&device),
838 MemoryBudget::with_limit(1024 * 1024),
839 Arc::clone(&runtime),
840 ));
841 let buf_a = manager.alloc::<u8>(64).expect("alloc a");
842 let mut buf_fresh = manager.alloc::<u8>(64).expect("alloc fresh");
843
844 let mut rec = LaunchRecorder::new_strict(launch_stream);
845 rec.read(&buf_a);
846 rec.write(&buf_fresh);
847 rec.preflight(&runtime).expect("preflight ok");
848 let _kernel_param = &mut buf_fresh;
850 rec.commit(&runtime).expect("commit ok");
851 }
852
853 #[test]
856 fn read_then_write_same_block_dedupes_to_read_write() {
857 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
858 return;
859 };
860 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
861 Arc::clone(&device),
862 MemoryBudget::with_limit(1024 * 1024),
863 Arc::clone(&runtime),
864 ));
865 let buf = manager.alloc::<u8>(64).expect("alloc");
866
867 let mut rec = LaunchRecorder::new_strict(launch_stream);
868 rec.read(&buf);
869 rec.write(&buf);
870 rec.preflight(&runtime).expect("preflight");
871 rec.commit(&runtime).expect("commit");
872 }
873
874 #[test]
881 fn dedup_keys_on_full_block_id_not_ptr_alone() {
882 let block_a = BlockId {
887 ptr: 0xdead_beef,
888 generation: Generation(1),
889 alloc_stream: StreamId::DEFAULT,
890 device_ordinal: 0,
891 };
892 let block_b = BlockId {
893 ptr: 0xdead_beef,
894 generation: Generation(2),
895 alloc_stream: StreamId::DEFAULT,
896 device_ordinal: 0,
897 };
898 let uses = vec![
899 RecordedUse {
900 block: block_a,
901 access: Access::Read,
902 label: "read",
903 },
904 RecordedUse {
905 block: block_b,
906 access: Access::Write,
907 label: "write",
908 },
909 ];
910 let deduped = dedup_uses(&uses);
911 assert_eq!(deduped.len(), 2, "ABA generations must NOT collapse");
912 assert_eq!(deduped[0].block.generation, Generation(1));
913 assert_eq!(deduped[0].access, Access::Read);
914 assert_eq!(deduped[1].block.generation, Generation(2));
915 assert_eq!(deduped[1].access, Access::Write);
916
917 let same_id = vec![
920 RecordedUse {
921 block: block_a,
922 access: Access::Read,
923 label: "read",
924 },
925 RecordedUse {
926 block: block_a,
927 access: Access::Write,
928 label: "write",
929 },
930 ];
931 let collapsed = dedup_uses(&same_id);
932 assert_eq!(collapsed.len(), 1);
933 assert_eq!(collapsed[0].access, Access::ReadWrite);
934 }
935
936 #[test]
937 fn dedup_distinguishes_allocation_stream_in_full_block_identity() {
938 let block_a = BlockId {
939 ptr: 0xdead_beef,
940 generation: Generation(1),
941 alloc_stream: StreamId(7),
942 device_ordinal: 0,
943 };
944 let block_b = BlockId {
945 alloc_stream: StreamId(11),
946 ..block_a
947 };
948 let uses = vec![
949 RecordedUse {
950 block: block_a,
951 access: Access::Read,
952 label: "read",
953 },
954 RecordedUse {
955 block: block_b,
956 access: Access::Write,
957 label: "write",
958 },
959 ];
960
961 let deduped = dedup_uses(&uses);
962 assert_eq!(deduped.len(), 2, "allocation streams are part of BlockId");
963 assert_eq!(deduped[0].block.alloc_stream, StreamId(7));
964 assert_eq!(deduped[1].block.alloc_stream, StreamId(11));
965 }
966
967 #[test]
968 fn read_device_block_snapshots_complete_identity_as_read() {
969 let block = DeviceBlock {
970 ptr: 0x1234,
971 device_ordinal: 2,
972 alloc_stream: StreamId(5),
973 bytes: 64,
974 align: 16,
975 tag: crate::device_runtime::AllocTag::UNTAGGED,
976 generation: Generation(9),
977 state: crate::device_runtime::BlockState::Live,
978 };
979 let expected = BlockId::from_block(&block);
980 let mut recorder = LaunchRecorder::new_strict(StreamId(8));
981
982 recorder.read_device_block(&block);
983
984 assert_eq!(recorder.uses.len(), 1);
985 assert_eq!(recorder.uses[0].block, expected);
986 assert_eq!(recorder.uses[0].access, Access::Read);
987 recorder.committed = true;
988 }
989
990 #[test]
991 fn read_block_identity_records_prevalidated_receipt_pointee() {
992 let identity = BlockId {
993 ptr: 0x9876,
994 generation: Generation(12),
995 alloc_stream: StreamId(4),
996 device_ordinal: 3,
997 };
998 let mut recorder = LaunchRecorder::new_strict(StreamId(6));
999
1000 recorder.read_block_identity(identity);
1001
1002 assert_eq!(recorder.uses.len(), 1);
1003 assert_eq!(recorder.uses[0].block, identity);
1004 assert_eq!(recorder.uses[0].access, Access::Read);
1005 recorder.committed = true;
1006 }
1007
1008 #[test]
1009 fn bound_strict_recorder_retains_runtime_and_domain_identity() {
1010 let _: fn(StreamId, Arc<XlogDeviceRuntime>, Arc<()>) -> LaunchRecorder =
1011 LaunchRecorder::new_strict_bound;
1012 }
1013
1014 #[test]
1015 fn bound_identity_uses_arc_ownership_not_value_equality() {
1016 let owner = Arc::new(());
1017 let same_owner = Arc::clone(&owner);
1018 let equal_value_foreign_owner = Arc::new(());
1019
1020 assert!(same_arc_identity(Some(&owner), &same_owner));
1021 assert!(!same_arc_identity(Some(&owner), &equal_value_foreign_owner));
1022 assert!(!same_arc_identity::<()>(None, &owner));
1023 }
1024
1025 #[test]
1026 fn bound_recorder_exposes_domain_check_and_arc_preflight() {
1027 let _: for<'a> fn(
1028 &'a mut LaunchRecorder,
1029 &Arc<XlogDeviceRuntime>,
1030 &Arc<()>,
1031 StreamId,
1032 ) -> &'a mut LaunchRecorder = LaunchRecorder::require_bound_domain;
1033 let _: fn(&mut LaunchRecorder, &Arc<XlogDeviceRuntime>) -> ResourceResult<()> =
1034 LaunchRecorder::preflight_bound;
1035 let _: fn(LaunchRecorder, &Arc<XlogDeviceRuntime>, &Arc<()>) -> ResourceResult<()> =
1036 LaunchRecorder::commit_bound;
1037 }
1038
1039 #[test]
1040 fn bound_commit_checks_arc_domain_before_finish_use_path() {
1041 let source = include_str!("launch.rs");
1042 let start = source
1043 .find("pub(crate) fn commit_bound")
1044 .expect("bound commit");
1045 let end = source[start..]
1046 .find("fn commit_inner")
1047 .map(|offset| start + offset)
1048 .expect("commit implementation");
1049 let bound_commit = &source[start..end];
1050 let runtime_check = bound_commit
1051 .find("same_arc_identity(self.bound_runtime.as_ref(), runtime)")
1052 .expect("runtime Arc check");
1053 let domain_check = bound_commit
1054 .find("same_arc_identity(self.bound_domain.as_ref(), domain)")
1055 .expect("domain Arc check");
1056 let finish_path = bound_commit
1057 .find("self.commit_inner(runtime.as_ref())")
1058 .expect("finish-use path");
1059 assert!(runtime_check < finish_path && domain_check < finish_path);
1060 }
1061
1062 #[test]
1063 fn read_column_owned_runtime_backed() {
1064 use crate::memory::CudaColumn;
1065 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
1066 return;
1067 };
1068 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
1069 Arc::clone(&device),
1070 MemoryBudget::with_limit(1024 * 1024),
1071 Arc::clone(&runtime),
1072 ));
1073 let slice = manager.alloc::<u8>(64).expect("alloc");
1074 let col = CudaColumn::owned(slice);
1075 assert!(col.runtime_block().is_some());
1076
1077 let mut rec = LaunchRecorder::new_strict(launch_stream);
1078 rec.read_column(&col);
1079 assert_eq!(rec.recorded_count(), 1);
1080 rec.preflight(&runtime).expect("preflight");
1081 rec.commit(&runtime).expect("commit");
1082 }
1083}