1use std::collections::HashMap;
21use std::fmt;
22use std::sync::Arc;
23
24use cudarc::driver::CudaStream;
25
26use crate::device_runtime::resource::MemoryUseGroup;
27use crate::device_runtime::{
28 Access, BlockId, BlockUse, Generation, ResourceError, ResourceResult, StreamId,
29 XlogDeviceRuntime,
30};
31use crate::memory::{
32 admit_memory_access, DeviceMemoryAccess, DeviceRead, DeviceWrite, MemoryOperationOwner,
33};
34use xlog_core::XlogError;
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum RecorderMode {
39 Permissive,
41 Strict,
43}
44
45#[non_exhaustive]
47#[derive(Debug)]
48pub enum LaunchEnqueueError<E> {
49 Preparation(ResourceError),
51 PreparationAndCleanup {
53 preparation: ResourceError,
54 cleanup: ResourceError,
55 },
56 Operation(E),
58 OperationAndCleanup {
60 operation: E,
61 cleanup: ResourceError,
62 },
63}
64
65impl<E: fmt::Display> fmt::Display for LaunchEnqueueError<E> {
66 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 Self::Preparation(error) => write!(formatter, "launch preparation failed: {error}"),
69 Self::PreparationAndCleanup {
70 preparation,
71 cleanup,
72 } => write!(
73 formatter,
74 "launch preparation failed: {preparation}; cleanup also failed: {cleanup}"
75 ),
76 Self::Operation(error) => write!(formatter, "launch operation failed: {error}"),
77 Self::OperationAndCleanup { operation, cleanup } => write!(
78 formatter,
79 "launch operation failed: {operation}; cleanup also failed: {cleanup}"
80 ),
81 }
82 }
83}
84
85impl<E> std::error::Error for LaunchEnqueueError<E>
86where
87 E: std::error::Error + 'static,
88{
89 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
90 match self {
91 Self::Preparation(error) => Some(error),
92 Self::PreparationAndCleanup { preparation, .. } => Some(preparation),
93 Self::Operation(error) => Some(error),
94 Self::OperationAndCleanup { operation, .. } => Some(operation),
95 }
96 }
97}
98
99impl LaunchEnqueueError<XlogError> {
100 pub(crate) fn into_xlog_error(self) -> XlogError {
101 match self {
102 Self::Operation(error) => error,
103 other => XlogError::Kernel(other.to_string()),
104 }
105 }
106}
107
108fn preparation_with_cleanup<E>(
109 preparation: ResourceError,
110 cleanup: ResourceResult<()>,
111) -> LaunchEnqueueError<E> {
112 match cleanup {
113 Ok(()) => LaunchEnqueueError::Preparation(preparation),
114 Err(cleanup) => LaunchEnqueueError::PreparationAndCleanup {
115 preparation,
116 cleanup,
117 },
118 }
119}
120
121fn operation_with_cleanup<E>(operation: E, cleanup: ResourceResult<()>) -> LaunchEnqueueError<E> {
122 match cleanup {
123 Ok(()) => LaunchEnqueueError::Operation(operation),
124 Err(cleanup) => LaunchEnqueueError::OperationAndCleanup { operation, cleanup },
125 }
126}
127
128pub struct LaunchRecorder {
160 launch_stream: StreamId,
161 mode: RecorderMode,
162 uses: Vec<BlockUse>,
166 accesses: Vec<DeviceMemoryAccess>,
167 strict_reject: Option<ResourceError>,
171 transaction: RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>,
172 submission: Option<crate::cuda_graph::StreamSubmissionPhase>,
174 bound_runtime: Option<Arc<XlogDeviceRuntime>>,
175 bound_domain: Option<Arc<()>>,
176}
177
178pub struct CudaEnqueue<'a> {
182 owner: &'a Arc<MemoryOperationOwner>,
183 submission: &'a crate::cuda_graph::StreamSubmissionPin<'a>,
184 execution_id: u64,
185}
186
187impl CudaEnqueue<'_> {
188 pub(crate) fn from_admission<'a>(
189 owner: &'a Arc<MemoryOperationOwner>,
190 submission: &'a crate::cuda_graph::StreamSubmissionPin<'a>,
191 ) -> ResourceResult<CudaEnqueue<'a>> {
192 Ok(CudaEnqueue {
193 owner,
194 submission,
195 execution_id: submission.validate_execution(owner.stream())?,
196 })
197 }
198 pub fn stream(&self) -> &Arc<CudaStream> {
199 self.owner.stream()
200 }
201
202 pub(crate) fn manifest(&self) -> &Arc<crate::memory::MemoryAccessManifest> {
203 self.owner.manifest()
204 }
205
206 pub(crate) fn completion(&self) -> Arc<crate::memory::OperationCompletion> {
207 Arc::clone(self.owner.completion())
208 }
209
210 pub(crate) fn submit<T>(
211 &self,
212 operation: impl FnOnce(
213 Option<&crate::cuda_graph::CaptureOwners>,
214 ) -> Result<T, cudarc::driver::DriverError>,
215 ) -> Result<T, cudarc::driver::DriverError> {
216 if self.submission.validate_execution(self.stream())? != self.execution_id {
217 return Err(cudarc::driver::DriverError(
218 cudarc::driver::sys::CUresult::CUDA_ERROR_INVALID_CONTEXT,
219 ));
220 }
221 self.submission.with_submission(operation)
222 }
223}
224
225#[must_use = "an enqueued launch must be committed or aborted"]
227pub struct EnqueuedLaunch {
228 recorder: LaunchRecorder,
229}
230
231impl EnqueuedLaunch {
232 pub fn commit(self) -> ResourceResult<()> {
234 self.recorder.commit()
235 }
236
237 pub fn abort(self) -> ResourceResult<()> {
239 self.recorder.abort()
240 }
241
242 pub(crate) fn commit_bound(self, domain: &Arc<()>) -> ResourceResult<()> {
243 self.recorder.commit_bound(domain)
244 }
245}
246
247#[derive(Clone, Copy, Debug, Eq, PartialEq)]
248enum EnqueuePhase {
249 NotStarted,
250 MayHaveEnqueued,
251}
252
253pub(crate) trait RecorderCleanup<U>: Send + Sync + 'static {
255 fn synchronize_retired(&self) -> ResourceResult<()>;
258
259 fn cancel_retired(&self, use_: U) -> ResourceResult<()>;
262}
263
264pub(crate) struct PreparedRecorderTransaction<R, U: Copy + 'static = BlockUse> {
265 owner: Arc<R>,
266 pending: Box<[U]>,
267 enqueue_phase: EnqueuePhase,
268}
269
270#[must_use = "an admitted transaction must be committed or aborted"]
271pub(crate) enum RecorderTransaction<R, U: Copy + 'static = BlockUse> {
272 Recording,
273 Prepared(PreparedRecorderTransaction<R, U>),
274 Terminal,
275}
276
277struct PreparedCleanupGuard<R: RecorderCleanup<U>, U: Copy + Send + 'static = BlockUse> {
278 owner: Option<Arc<R>>,
279 pending: Option<Box<[U]>>,
280 remaining_start: usize,
281 needs_completion: bool,
282 armed: bool,
283}
284
285impl<R: RecorderCleanup<U>, U: Copy + Send + 'static> PreparedCleanupGuard<R, U> {
286 fn new(prepared: PreparedRecorderTransaction<R, U>) -> Self {
287 Self {
288 owner: Some(prepared.owner),
289 pending: Some(prepared.pending),
290 remaining_start: 0,
291 needs_completion: prepared.enqueue_phase == EnqueuePhase::MayHaveEnqueued,
292 armed: true,
293 }
294 }
295
296 fn owner(&self) -> &R {
297 self.owner.as_deref().expect("prepared owner is present")
298 }
299
300 fn pending(&self) -> &[U] {
301 self.pending.as_deref().expect("prepared uses are present")
302 }
303
304 fn set_remaining_start(&mut self, remaining_start: usize) {
305 self.remaining_start = remaining_start;
306 }
307
308 fn disarm(&mut self) {
309 self.armed = false;
310 }
311
312 fn quarantine(mut self) {
313 let (owner, pending) = self.take_pending();
314 self.armed = false;
315 quarantine_owner_and_pending(owner, pending, self.remaining_start, self.needs_completion);
316 }
317
318 fn take_pending(&mut self) -> (Arc<R>, Box<[U]>) {
319 let owner = self.owner.take().expect("prepared owner is present");
320 let pending = self.pending.take().expect("prepared uses are present");
321 (owner, pending)
323 }
324}
325
326impl<R: RecorderCleanup<U>, U: Copy + Send + 'static> Drop for PreparedCleanupGuard<R, U> {
327 fn drop(&mut self) {
328 if !self.armed {
329 return;
330 }
331 let (owner, pending) = self.take_pending();
332 quarantine_owner_and_pending(owner, pending, self.remaining_start, self.needs_completion);
333 }
334}
335
336impl<R: RecorderCleanup<U>, U: Copy + Send + 'static> RecorderTransaction<R, U> {
337 pub(crate) fn from_admitted(owner: Arc<R>, pending: Box<[U]>) -> Self {
340 Self::Prepared(PreparedRecorderTransaction {
341 owner,
342 pending,
343 enqueue_phase: EnqueuePhase::NotStarted,
344 })
345 }
346
347 fn recording() -> Self {
348 Self::Recording
349 }
350
351 fn is_recording(&self) -> bool {
352 matches!(self, Self::Recording)
353 }
354
355 pub(crate) fn is_prepared(&self) -> bool {
356 matches!(self, Self::Prepared(_))
357 }
358
359 pub(crate) fn prepared_owner(&self) -> Option<&Arc<R>> {
360 match self {
361 Self::Prepared(prepared) => Some(&prepared.owner),
362 _ => None,
363 }
364 }
365
366 #[cfg(test)]
367 fn is_terminal(&self) -> bool {
368 matches!(self, Self::Terminal)
369 }
370
371 fn mark_terminal(&mut self) {
372 *self = Self::Terminal;
373 }
374
375 fn mark_may_have_enqueued(&mut self) -> ResourceResult<()> {
376 let Self::Prepared(prepared) = self else {
377 return Err(ResourceError::StreamMisuse(
378 "launch enqueue requires successful preflight".to_string(),
379 ));
380 };
381 match prepared.enqueue_phase {
382 EnqueuePhase::NotStarted => {
383 prepared.enqueue_phase = EnqueuePhase::MayHaveEnqueued;
384 Ok(())
385 }
386 EnqueuePhase::MayHaveEnqueued => Err(ResourceError::StreamMisuse(
387 "launch enqueue is one-shot".to_string(),
388 )),
389 }
390 }
391
392 fn prepare_enqueue_with<Prepare>(&mut self, prepare: Prepare) -> ResourceResult<()>
393 where
394 Prepare: FnOnce(&R) -> ResourceResult<()>,
395 {
396 self.mark_may_have_enqueued()?;
397 let Self::Prepared(prepared) = self else {
400 unreachable!("successful enqueue marking preserves the prepared owner")
401 };
402 prepare(&prepared.owner)
403 }
404
405 pub(crate) fn enqueue_operation_with<E, Admission, Admit, Prepare, Operation, Sync, Cancel>(
406 &mut self,
407 admit: Admit,
408 prepare: Prepare,
409 operation: Operation,
410 sync: Sync,
411 cancel: Cancel,
412 ) -> Result<(), LaunchEnqueueError<E>>
413 where
414 Admit: FnOnce() -> ResourceResult<Admission>,
415 Prepare: FnOnce(&R) -> ResourceResult<()>,
416 Operation: FnOnce(&Admission) -> Result<(), E>,
417 Sync: FnOnce(&R) -> ResourceResult<()>,
418 Cancel: FnOnce(&R, &[U]) -> ResourceResult<()>,
419 {
420 let admission = match admit() {
424 Ok(admission) => admission,
425 Err(preparation) => {
426 return Err(preparation_with_cleanup(
427 preparation,
428 self.abort_with(sync, cancel),
429 ));
430 }
431 };
432 if let Err(preparation) = self.prepare_enqueue_with(prepare) {
433 drop(admission);
434 return Err(preparation_with_cleanup(
435 preparation,
436 self.abort_with(sync, cancel),
437 ));
438 }
439 let result = operation(&admission);
440 drop(admission);
443 match result {
444 Ok(()) => Ok(()),
445 Err(operation) => Err(operation_with_cleanup(
446 operation,
447 self.abort_with(sync, cancel),
448 )),
449 }
450 }
451
452 pub(crate) fn abort_with<Sync, Cancel>(
453 &mut self,
454 sync: Sync,
455 cancel: Cancel,
456 ) -> ResourceResult<()>
457 where
458 Sync: FnOnce(&R) -> ResourceResult<()>,
459 Cancel: FnOnce(&R, &[U]) -> ResourceResult<()>,
460 {
461 let transaction = std::mem::replace(self, Self::Terminal);
462 let Self::Prepared(prepared) = transaction else {
463 return Err(ResourceError::StreamMisuse(
464 "LaunchRecorder::abort requires successful preflight".to_string(),
465 ));
466 };
467 let enqueue_phase = prepared.enqueue_phase;
468 let mut guard = PreparedCleanupGuard::new(prepared);
469
470 if enqueue_phase == EnqueuePhase::MayHaveEnqueued {
471 if let Err(sync_error) = sync(guard.owner()) {
472 guard.quarantine();
473 return Err(ResourceError::Driver(format!(
474 "LaunchRecorder::abort could not synchronize possible launch work; \
475 runtime and reservations were quarantined: {sync_error}"
476 )));
477 }
478 guard.needs_completion = false;
479 }
480 if guard.pending().is_empty() {
481 guard.disarm();
482 return Ok(());
483 }
484 if let Err(cancel_error) = cancel(guard.owner(), guard.pending()) {
485 guard.quarantine();
486 return Err(ResourceError::Driver(format!(
487 "LaunchRecorder::abort could not cancel prepared reservations; \
488 runtime and reservations were quarantined: {cancel_error}"
489 )));
490 }
491 guard.disarm();
492 Ok(())
493 }
494
495 pub(crate) fn commit_with<Finish, Sync, Cancel>(
496 &mut self,
497 mut finish: Finish,
498 sync: Sync,
499 cancel: Cancel,
500 ) -> ResourceResult<()>
501 where
502 Finish: FnMut(&R, U) -> ResourceResult<()>,
503 Sync: FnOnce(&R) -> ResourceResult<()>,
504 Cancel: FnOnce(&R, &[U]) -> ResourceResult<()>,
505 {
506 let transaction = std::mem::replace(self, Self::Terminal);
507 let Self::Prepared(prepared) = transaction else {
508 return Err(ResourceError::StreamMisuse(
509 "LaunchRecorder::commit requires successful preflight".to_string(),
510 ));
511 };
512 let enqueue_phase = prepared.enqueue_phase;
513 let mut guard = PreparedCleanupGuard::new(prepared);
514
515 if enqueue_phase == EnqueuePhase::NotStarted && !guard.pending().is_empty() {
516 let error = ResourceError::StreamMisuse(
517 "LaunchRecorder::commit requires an executed enqueue callback".to_string(),
518 );
519 if let Err(cancel_error) = cancel(guard.owner(), guard.pending()) {
520 guard.quarantine();
521 return Err(cleanup_failure(
522 "commit before enqueue boundary",
523 &error,
524 &cancel_error,
525 ));
526 }
527 guard.disarm();
528 return Err(error);
529 }
530
531 let pending_len = guard.pending().len();
532 for index in 0..pending_len {
533 let use_ = guard.pending()[index];
534 guard.set_remaining_start(index);
535 if let Err(finish_error) = finish(guard.owner(), use_) {
536 if let Err(sync_error) = sync(guard.owner()) {
537 guard.quarantine();
538 return Err(cleanup_failure(
539 "commit synchronization",
540 &finish_error,
541 &sync_error,
542 ));
543 }
544 guard.needs_completion = false;
545 if let Err(cancel_error) = cancel(guard.owner(), &guard.pending()[index..]) {
546 guard.quarantine();
547 return Err(cleanup_failure(
548 "commit tail cancellation",
549 &finish_error,
550 &cancel_error,
551 ));
552 }
553 guard.disarm();
554 return Err(finish_error);
555 }
556 guard.set_remaining_start(index + 1);
557 }
558 guard.disarm();
559 Ok(())
560 }
561}
562
563fn cleanup_failure(stage: &str, primary: &ResourceError, cleanup: &ResourceError) -> ResourceError {
564 ResourceError::Driver(format!(
565 "LaunchRecorder {stage} failed after {primary}; runtime and remaining \
566 reservations were quarantined: {cleanup}"
567 ))
568}
569
570fn quarantine_owner_and_pending<R: RecorderCleanup<U>, U: Copy + Send + 'static>(
574 owner: Arc<R>,
575 pending: Box<[U]>,
576 mut remaining_start: usize,
577 mut needs_completion: bool,
578) {
579 let mut retained = Some(owner);
580 let attempted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
583 crate::cuda_graph::retry_retirement_after_stream_captures(move || {
584 let Some(owner) = retained.as_ref() else {
585 return true;
586 };
587 if needs_completion {
588 if owner.synchronize_retired().is_err() {
589 return false;
590 }
591 needs_completion = false;
592 }
593 while let Some(use_) = pending.get(remaining_start) {
594 if owner.cancel_retired(*use_).is_err() {
595 return false;
596 }
597 remaining_start += 1;
598 }
599 drop(retained.take());
600 true
601 });
602 }));
603 if let Err(payload) = attempted {
604 std::mem::forget(payload);
608 }
609}
610
611impl LaunchRecorder {
612 pub fn new_permissive(launch_stream: StreamId) -> Self {
614 Self::new(launch_stream, RecorderMode::Permissive)
615 }
616
617 pub fn new_strict(launch_stream: StreamId) -> Self {
619 Self::new(launch_stream, RecorderMode::Strict)
620 }
621
622 pub(crate) fn new_strict_bound(
623 launch_stream: StreamId,
624 runtime: Arc<XlogDeviceRuntime>,
625 domain: Arc<()>,
626 ) -> Self {
627 let mut recorder = Self::new(launch_stream, RecorderMode::Strict);
628 recorder.bound_runtime = Some(runtime);
629 recorder.bound_domain = Some(domain);
630 recorder
631 }
632
633 fn new(launch_stream: StreamId, mode: RecorderMode) -> Self {
634 Self {
635 launch_stream,
636 mode,
637 uses: Vec::new(),
638 accesses: Vec::new(),
639 strict_reject: None,
640 transaction: RecorderTransaction::recording(),
641 submission: None,
642 bound_runtime: None,
643 bound_domain: None,
644 }
645 }
646
647 pub fn launch_stream(&self) -> StreamId {
649 self.launch_stream
650 }
651
652 pub fn mode(&self) -> RecorderMode {
654 self.mode
655 }
656
657 pub(crate) fn require_bound_domain<'a>(
658 &'a mut self,
659 runtime: &Arc<XlogDeviceRuntime>,
660 domain: &Arc<()>,
661 launch_stream: StreamId,
662 ) -> &'a mut Self {
663 let matches = self.mode == RecorderMode::Strict
664 && self.launch_stream == launch_stream
665 && same_arc_identity(self.bound_runtime.as_ref(), runtime)
666 && same_arc_identity(self.bound_domain.as_ref(), domain);
667 if !matches && self.strict_reject.is_none() {
668 self.strict_reject = Some(ResourceError::StreamMisuse(
669 "LaunchRecorder: recorder is not bound to the resident execution domain"
670 .to_string(),
671 ));
672 }
673 self
674 }
675
676 fn note_identity(
677 &mut self,
678 label: &'static str,
679 block: Option<(BlockId, usize)>,
680 access: Access,
681 ) -> &mut Self {
682 if self.strict_reject.is_some() {
683 return self;
684 }
685 if !self.transaction.is_recording() {
686 self.strict_reject = Some(ResourceError::StreamMisuse(format!(
687 "LaunchRecorder::{label}: recorded after preflight"
688 )));
689 return self;
690 }
691 if let Some((block, bytes)) = block {
692 self.uses.push(BlockUse {
693 block,
694 bytes,
695 access,
696 });
697 } else {
698 self.strict_reject = Some(ResourceError::StreamMisuse(format!(
699 "LaunchRecorder::{label}: pointer-table entry has neither an allocation identity nor a storage owner"
700 )));
701 }
702 self
703 }
704
705 fn note_memory(
706 &mut self,
707 label: &'static str,
708 access: ResourceResult<DeviceMemoryAccess>,
709 ) -> &mut Self {
710 if self.strict_reject.is_some() {
711 return self;
712 }
713 if !self.transaction.is_recording() {
714 self.strict_reject = Some(ResourceError::StreamMisuse(format!(
715 "LaunchRecorder::{label}: recorded after preflight"
716 )));
717 return self;
718 }
719 match access {
720 Ok(access) => self.accesses.push(access),
721 Err(error) => self.strict_reject = Some(error),
722 }
723 self
724 }
725
726 pub fn read<T: cudarc::driver::DeviceRepr>(
728 &mut self,
729 slice: &(impl DeviceRead<T> + ?Sized),
730 ) -> &mut Self {
731 self.note_memory("read", slice.device_view().access(Access::Read))
732 }
733
734 #[cfg(test)]
740 pub(crate) fn read_device_block(
741 &mut self,
742 block: &crate::device_runtime::DeviceBlock,
743 ) -> &mut Self {
744 self.note_identity(
745 "read_device_block",
746 Some((BlockId::from_block(block), block.bytes)),
747 Access::Read,
748 )
749 }
750
751 #[cfg(test)]
753 pub(crate) fn read_block_identity(&mut self, block: (BlockId, usize)) -> &mut Self {
754 self.note_identity("read_block_identity", Some(block), Access::Read)
755 }
756
757 pub(crate) fn read_optional_block_identity(
758 &mut self,
759 block: Option<(BlockId, usize)>,
760 ) -> &mut Self {
761 self.note_identity("read_optional_block_identity", block, Access::Read)
762 }
763
764 pub fn write<T: cudarc::driver::DeviceRepr>(
767 &mut self,
768 slice: &(impl DeviceWrite<T> + ?Sized),
769 ) -> &mut Self {
770 self.note_memory("write", slice.device_view().access(Access::Write))
771 }
772
773 pub fn read_write<T: cudarc::driver::DeviceRepr>(
775 &mut self,
776 slice: &(impl DeviceWrite<T> + ?Sized),
777 ) -> &mut Self {
778 self.note_memory("read_write", slice.device_view().access(Access::ReadWrite))
779 }
780
781 pub fn read_column(&mut self, col: &crate::memory::CudaColumn) -> &mut Self {
783 self.read(col)
784 }
785
786 pub fn write_column(&mut self, col: &crate::memory::CudaColumn) -> &mut Self {
789 self.write(col)
790 }
791
792 pub fn recorded_count(&self) -> usize {
794 self.uses.len() + self.accesses.len()
795 }
796
797 pub(crate) fn preflight(&mut self, runtime: &Arc<XlogDeviceRuntime>) -> ResourceResult<()> {
802 if !self.transaction.is_recording() {
803 return Err(ResourceError::StreamMisuse(
804 "LaunchRecorder::preflight is one-shot".into(),
805 ));
806 }
807 self.transaction.mark_terminal();
808 if let Some(bound_runtime) = &self.bound_runtime {
809 if !Arc::ptr_eq(bound_runtime, runtime) {
810 return Err(ResourceError::StreamMisuse(
811 "LaunchRecorder::preflight: bound recorder received a foreign runtime".into(),
812 ));
813 }
814 }
815 if let Some(error) = self.strict_reject.take() {
816 return Err(error);
817 }
818 let stream = runtime
819 .stream_pool()
820 .resolve(self.launch_stream)
821 .ok_or_else(|| {
822 ResourceError::StreamMisuse(
823 "LaunchRecorder::preflight: launch stream is not owned by the runtime".into(),
824 )
825 })?;
826 if self
827 .accesses
828 .iter()
829 .any(|access| access.context() != stream.context().cu_ctx() as usize)
830 {
831 return Err(ResourceError::StreamMisuse(
832 "LaunchRecorder::preflight: allocation and launch CUDA contexts differ".into(),
833 ));
834 }
835 let submission = crate::cuda_graph::acquire_stream_submission_phase(&stream)?;
836 self.transaction = admit_memory_access(
837 stream,
838 self.accesses.clone(),
839 Some(Arc::clone(runtime)),
840 &dedup_uses(&self.uses),
841 submission.execution_id(),
842 )?;
843 self.submission = Some(submission);
844 Ok(())
845 }
846
847 pub unsafe fn enqueue<E, F>(
857 mut self,
858 runtime: &Arc<XlogDeviceRuntime>,
859 operation: F,
860 ) -> Result<EnqueuedLaunch, LaunchEnqueueError<E>>
861 where
862 F: FnOnce(&CudaEnqueue<'_>) -> Result<(), E>,
863 {
864 if self.bound_runtime.is_some() || self.bound_domain.is_some() {
865 let preparation = ResourceError::StreamMisuse(
866 "LaunchRecorder::enqueue does not accept an execution-domain-bound recorder"
867 .to_string(),
868 );
869 return Err(self.reject_enqueue(preparation));
870 }
871 if !self.transaction.is_recording() {
872 let preparation = ResourceError::StreamMisuse(
873 "LaunchRecorder::enqueue requires a recorder in the recording phase".to_string(),
874 );
875 return Err(self.reject_enqueue(preparation));
876 }
877
878 let Some(stream) = runtime.stream_pool().resolve(self.launch_stream) else {
879 let preparation = ResourceError::StreamMisuse(format!(
880 "LaunchRecorder::enqueue: stream {:?} does not resolve through the supplied runtime",
881 self.launch_stream
882 ));
883 return Err(self.reject_enqueue(preparation));
884 };
885
886 self.preflight(runtime)
887 .map_err(LaunchEnqueueError::Preparation)?;
888
889 unsafe { self.enqueue_prepared_with(stream.as_ref(), operation) }
891 }
892
893 pub(crate) unsafe fn enqueue_bound<E, F>(
901 mut self,
902 runtime: &Arc<XlogDeviceRuntime>,
903 domain: &Arc<()>,
904 launch_stream: StreamId,
905 stream: &CudaStream,
906 operation: F,
907 ) -> Result<EnqueuedLaunch, LaunchEnqueueError<E>>
908 where
909 F: FnOnce(&CudaEnqueue<'_>) -> Result<(), E>,
910 {
911 self.require_bound_domain(runtime, domain, launch_stream);
912 if !self.transaction.is_recording() {
913 let preparation = ResourceError::StreamMisuse(
914 "LaunchRecorder::enqueue_bound requires a recorder in the recording phase"
915 .to_string(),
916 );
917 return Err(self.reject_enqueue(preparation));
918 }
919 self.preflight(runtime)
920 .map_err(LaunchEnqueueError::Preparation)?;
921
922 unsafe { self.enqueue_prepared_with(stream, operation) }
924 }
925
926 pub(crate) unsafe fn enqueue_prepared_with<E, F>(
932 self,
933 stream: &CudaStream,
934 operation: F,
935 ) -> Result<EnqueuedLaunch, LaunchEnqueueError<E>>
936 where
937 F: FnOnce(&CudaEnqueue<'_>) -> Result<(), E>,
938 {
939 let mut enqueued = EnqueuedLaunch { recorder: self };
940 let submission = enqueued
941 .recorder
942 .submission
943 .as_ref()
944 .expect("launch phase admitted");
945 let owner = Arc::clone(
946 enqueued
947 .recorder
948 .transaction
949 .prepared_owner()
950 .expect("memory admitted"),
951 );
952 if !std::ptr::eq(stream, owner.stream().as_ref()) {
953 return Err(enqueued
954 .recorder
955 .reject_enqueue(ResourceError::StreamMisuse(
956 "enqueue used a different admitted stream".into(),
957 )));
958 }
959 let result = enqueued.recorder.transaction.enqueue_operation_with(
960 || {
961 let pin = submission.pin()?;
962 let execution_id = pin.validate_execution(owner.stream())?;
963 owner.bind_submission(&pin)?;
964 Ok((pin, execution_id))
965 },
966 MemoryOperationOwner::prepare,
967 |(pin, execution_id)| {
968 pin.with_serialized_submission(|| {
969 operation(&CudaEnqueue {
970 owner: &owner,
971 submission: pin,
972 execution_id: *execution_id,
973 })
974 })
975 },
976 MemoryOperationOwner::synchronize,
977 MemoryOperationOwner::cancel,
978 );
979 match result {
980 Ok(()) => Ok(enqueued),
981 Err(error) => Err(error),
982 }
983 }
984
985 fn reject_enqueue<E>(&mut self, preparation: ResourceError) -> LaunchEnqueueError<E> {
986 if !self.transaction.is_prepared() {
987 self.transaction.mark_terminal();
988 return LaunchEnqueueError::Preparation(preparation);
989 }
990
991 preparation_with_cleanup(preparation, self.abort_transaction())
992 }
993
994 pub(crate) fn abort(mut self) -> ResourceResult<()> {
996 self.abort_transaction()
997 }
998
999 pub(crate) fn commit(mut self) -> ResourceResult<()> {
1019 if self.bound_runtime.is_some() || self.bound_domain.is_some() {
1020 let error = ResourceError::StreamMisuse(
1021 "LaunchRecorder::commit: domain-bound recorder requires commit_bound".to_string(),
1022 );
1023 return Err(self.fail_with_abort(error));
1024 }
1025 self.commit_inner()
1026 }
1027
1028 pub(crate) fn commit_bound(mut self, domain: &Arc<()>) -> ResourceResult<()> {
1029 if !same_arc_identity(self.bound_domain.as_ref(), domain) {
1030 let error = ResourceError::StreamMisuse(
1031 "LaunchRecorder::commit_bound: foreign or unbound execution domain".to_string(),
1032 );
1033 return Err(self.fail_with_abort(error));
1034 }
1035 self.commit_inner()
1036 }
1037
1038 fn commit_inner(&mut self) -> ResourceResult<()> {
1039 if let Some(err) = self.strict_reject.take() {
1040 return Err(self.fail_with_abort(err));
1041 }
1042 if self.recorded_count() == 0 && self.transaction.is_recording() {
1043 self.transaction.mark_terminal();
1044 return Ok(());
1045 }
1046 if !self.transaction.is_prepared() {
1047 self.transaction.mark_terminal();
1048 return Err(ResourceError::StreamMisuse(
1049 "LaunchRecorder::commit: non-empty recorder reached commit without \
1050 a successful preflight. The caller MUST call preflight(&runtime) \
1051 BEFORE enqueueing CUDA work; otherwise commit-time failures leave \
1052 unprotected work in flight. See the preflight + commit contract \
1053 in the LaunchRecorder doc"
1054 .to_string(),
1055 ));
1056 }
1057
1058 self.transaction.commit_with(
1059 MemoryOperationOwner::finish,
1060 MemoryOperationOwner::synchronize,
1061 MemoryOperationOwner::cancel,
1062 )
1063 }
1064
1065 fn abort_transaction(&mut self) -> ResourceResult<()> {
1066 self.transaction.abort_with(
1067 MemoryOperationOwner::synchronize,
1068 MemoryOperationOwner::cancel,
1069 )
1070 }
1071
1072 fn fail_with_abort(&mut self, primary: ResourceError) -> ResourceError {
1073 if !self.transaction.is_prepared() {
1074 self.transaction.mark_terminal();
1075 return primary;
1076 }
1077 match self.abort_transaction() {
1078 Ok(()) => primary,
1079 Err(cleanup) => cleanup_failure("error cleanup", &primary, &cleanup),
1080 }
1081 }
1082}
1083
1084fn dedup_uses(uses: &[BlockUse]) -> Vec<BlockUse> {
1101 let mut by_id: HashMap<(u64, Generation, StreamId, u32, usize), usize> =
1102 HashMap::with_capacity(uses.len());
1103 let mut deduped: Vec<BlockUse> = Vec::with_capacity(uses.len());
1104 for use_ in uses {
1105 let key = (
1106 use_.block.ptr,
1107 use_.block.generation,
1108 use_.block.alloc_stream,
1109 use_.block.device_ordinal,
1110 use_.bytes,
1111 );
1112 match by_id.get(&key) {
1113 Some(&idx) => {
1114 deduped[idx].access = combine_access(deduped[idx].access, use_.access);
1115 }
1116 None => {
1117 by_id.insert(key, deduped.len());
1118 deduped.push(*use_);
1119 }
1120 }
1121 }
1122 deduped
1123}
1124
1125pub(crate) fn combine_access(a: Access, b: Access) -> Access {
1127 match (a, b) {
1128 (Access::ReadWrite, _) | (_, Access::ReadWrite) => Access::ReadWrite,
1129 (Access::Read, Access::Write) | (Access::Write, Access::Read) => Access::ReadWrite,
1130 (Access::Read, Access::Read) => Access::Read,
1131 (Access::Write, Access::Write) => Access::Write,
1132 }
1133}
1134
1135fn same_arc_identity<T>(bound: Option<&Arc<T>>, expected: &Arc<T>) -> bool {
1136 bound.is_some_and(|bound| Arc::ptr_eq(bound, expected))
1137}
1138
1139fn catch_drop_cleanup<F>(cleanup: F) -> std::thread::Result<ResourceResult<()>>
1140where
1141 F: FnOnce() -> ResourceResult<()>,
1142{
1143 std::panic::catch_unwind(std::panic::AssertUnwindSafe(cleanup))
1144}
1145
1146#[cfg(debug_assertions)]
1147fn best_effort_drop_diagnostic(arguments: fmt::Arguments<'_>) {
1148 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1149 use std::io::Write;
1150
1151 let mut stderr = std::io::stderr().lock();
1152 let _ = stderr.write_fmt(arguments);
1153 let _ = stderr.write_all(b"\n");
1154 }));
1155}
1156
1157impl Drop for LaunchRecorder {
1158 fn drop(&mut self) {
1159 if self.transaction.is_prepared() {
1160 match catch_drop_cleanup(|| self.abort_transaction()) {
1161 Ok(Ok(())) => {}
1162 Ok(Err(error)) => {
1163 #[cfg(debug_assertions)]
1164 best_effort_drop_diagnostic(format_args!(
1165 "[xlog_cuda::launch] LaunchRecorder drop cleanup failed; \
1166 runtime was quarantined: {error}"
1167 ));
1168 }
1169 Err(_) => {
1170 #[cfg(debug_assertions)]
1171 best_effort_drop_diagnostic(format_args!(
1172 "[xlog_cuda::launch] LaunchRecorder drop cleanup panicked; \
1173 runtime was quarantined"
1174 ));
1175 }
1176 }
1177 } else if self.transaction.is_recording() && self.recorded_count() != 0 {
1178 #[cfg(debug_assertions)]
1179 best_effort_drop_diagnostic(format_args!(
1180 "[xlog_cuda::launch] LaunchRecorder dropped without enqueue: \
1181 {} uses on launch_stream={} (mode={:?}) were not prepared",
1182 self.recorded_count(),
1183 self.launch_stream.0,
1184 self.mode,
1185 ));
1186 }
1187 }
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192 use super::*;
1193 use crate::device_runtime::{
1194 AsyncCudaResource, DeviceBlock, DeviceMemoryResource, DirectCudaResource, StreamPool,
1195 };
1196 use crate::CudaDevice;
1197 use std::sync::atomic::{AtomicBool, Ordering};
1198 use std::sync::{Arc, Mutex};
1199 use xlog_core::MemoryBudget;
1200
1201 #[derive(Default)]
1202 struct TestTransactionOwner {
1203 events: Arc<Mutex<Vec<&'static str>>>,
1204 retirement_ready: Arc<AtomicBool>,
1205 retired_uses: Arc<Mutex<Vec<BlockUse>>>,
1206 retirement_panics: AtomicBool,
1207 retirement_payload_panics: AtomicBool,
1208 cancel_second_fails: AtomicBool,
1209 }
1210
1211 impl RecorderCleanup<BlockUse> for TestTransactionOwner {
1212 fn synchronize_retired(&self) -> ResourceResult<()> {
1213 if self.retirement_payload_panics.swap(false, Ordering::AcqRel) {
1214 struct PanickingPayload;
1215 impl Drop for PanickingPayload {
1216 fn drop(&mut self) {
1217 panic!("secondary payload destructor panic");
1218 }
1219 }
1220 std::panic::panic_any(PanickingPayload);
1221 }
1222 assert!(
1223 !self.retirement_panics.swap(false, Ordering::AcqRel),
1224 "retirement panic"
1225 );
1226 if !self.retirement_ready.load(Ordering::Acquire) {
1227 return Err(ResourceError::Driver("completion remains unknown".into()));
1228 }
1229 self.events.lock().unwrap().push("retire_sync");
1230 Ok(())
1231 }
1232
1233 fn cancel_retired(&self, use_: BlockUse) -> ResourceResult<()> {
1234 if !self.retirement_ready.load(Ordering::Acquire) {
1235 return Err(ResourceError::Driver(
1236 "cancellation remains unavailable".into(),
1237 ));
1238 }
1239 if use_.block.ptr == 0x2000 && self.cancel_second_fails.load(Ordering::Acquire) {
1240 return Err(ResourceError::Driver(
1241 "second cancellation unavailable".into(),
1242 ));
1243 }
1244 self.retired_uses.lock().unwrap().push(use_);
1245 Ok(())
1246 }
1247 }
1248
1249 #[test]
1250 fn failed_abort_retires_exact_owner_after_origin_thread_exits() {
1251 let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
1252 let owner = Arc::new(TestTransactionOwner::default());
1253 let witness = Arc::downgrade(&owner);
1254 let ready = Arc::clone(&owner.retirement_ready);
1255 let retired = Arc::clone(&owner.retired_uses);
1256 std::thread::spawn(move || {
1257 let mut transaction = RecorderTransaction::from_admitted(
1258 owner,
1259 Box::new([test_use(0x1000), test_use(0x2000)]),
1260 );
1261 transaction.mark_may_have_enqueued().unwrap();
1262 let error = transaction
1263 .abort_with(
1264 |_| Err(ResourceError::Driver("original wait failed".into())),
1265 |_, _| panic!("unknown completion must not cancel"),
1266 )
1267 .unwrap_err();
1268 assert!(error.to_string().contains("original wait failed"));
1269 })
1270 .join()
1271 .unwrap();
1272 crate::cuda_graph::reap_capture_retirements();
1273 assert!(witness.upgrade().is_some());
1274 assert!(retired.lock().unwrap().is_empty());
1275
1276 ready.store(true, Ordering::Release);
1277 crate::cuda_graph::reap_capture_retirements();
1278 assert!(
1279 witness.upgrade().is_none(),
1280 "completed owner was permanently abandoned"
1281 );
1282 assert_eq!(
1283 *retired.lock().unwrap(),
1284 [test_use(0x1000), test_use(0x2000)]
1285 );
1286 crate::cuda_graph::reap_capture_retirements();
1287 assert_eq!(retired.lock().unwrap().len(), 2);
1288 }
1289
1290 #[test]
1291 fn failed_commit_retires_only_unfinished_tail_without_repeating_completed_wait() {
1292 let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
1293 let owner = Arc::new(TestTransactionOwner::default());
1294 let mut transaction = RecorderTransaction::from_admitted(
1295 Arc::clone(&owner),
1296 Box::new([test_use(0x1000), test_use(0x2000), test_use(0x3000)]),
1297 );
1298 transaction.mark_may_have_enqueued().unwrap();
1299 let result = transaction.commit_with(
1300 |owner, use_| {
1301 if use_.block.ptr == 0x2000 {
1302 return Err(ResourceError::Driver("finish failed".into()));
1303 }
1304 owner.retired_uses.lock().unwrap().push(use_);
1305 Ok(())
1306 },
1307 |_| Ok(()),
1308 |_, _| Err(ResourceError::Driver("cancel failed".into())),
1309 );
1310 assert!(result.is_err());
1311 owner.retirement_ready.store(true, Ordering::Release);
1312 owner.cancel_second_fails.store(true, Ordering::Release);
1313 crate::cuda_graph::reap_capture_retirements();
1314 assert_eq!(*owner.retired_uses.lock().unwrap(), [test_use(0x1000)]);
1315 owner.cancel_second_fails.store(false, Ordering::Release);
1316 crate::cuda_graph::reap_capture_retirements();
1317 crate::cuda_graph::reap_capture_retirements();
1318 assert!(
1319 owner.events.lock().unwrap().is_empty(),
1320 "successful wait was repeated"
1321 );
1322 assert_eq!(
1323 *owner.retired_uses.lock().unwrap(),
1324 [test_use(0x1000), test_use(0x2000), test_use(0x3000)]
1325 );
1326 }
1327
1328 #[cfg(panic = "unwind")]
1329 #[test]
1330 fn failed_abort_preserves_original_panic_when_eager_retirement_also_panics() {
1331 let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
1332 let owner = Arc::new(TestTransactionOwner::default());
1333 owner.retirement_panics.store(true, Ordering::Release);
1334 let witness = Arc::downgrade(&owner);
1335 let ready = Arc::clone(&owner.retirement_ready);
1336 let mut transaction =
1337 RecorderTransaction::from_admitted(owner, Box::new([test_use(0x1000)]));
1338 transaction.mark_may_have_enqueued().unwrap();
1339 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1340 let _ = transaction.abort_with(|_| panic!("original wait panic"), |_, _| Ok(()));
1341 }))
1342 .unwrap_err();
1343 assert_eq!(panic.downcast_ref::<&str>(), Some(&"original wait panic"));
1344 assert!(witness.upgrade().is_some());
1345 ready.store(true, Ordering::Release);
1346 crate::cuda_graph::reap_capture_retirements();
1347 assert!(witness.upgrade().is_none());
1348 }
1349
1350 #[cfg(panic = "unwind")]
1351 #[test]
1352 fn failed_abort_contains_a_secondary_panicking_payload_destructor() {
1353 let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
1354 let owner = Arc::new(TestTransactionOwner::default());
1355 owner
1356 .retirement_payload_panics
1357 .store(true, Ordering::Release);
1358 let witness = Arc::downgrade(&owner);
1359 let ready = Arc::clone(&owner.retirement_ready);
1360 let mut transaction =
1361 RecorderTransaction::from_admitted(owner, Box::new([test_use(0x1000)]));
1362 transaction.mark_may_have_enqueued().unwrap();
1363 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1364 transaction.abort_with(
1365 |_| Err(ResourceError::Driver("original wait error".into())),
1366 |_, _| panic!("unknown completion must not cancel"),
1367 )
1368 }));
1369 ready.store(true, Ordering::Release);
1371 crate::cuda_graph::reap_capture_retirements();
1372 assert!(witness.upgrade().is_none());
1373 assert!(
1374 result.is_ok(),
1375 "secondary panic payload escaped retirement containment"
1376 );
1377 assert!(result
1378 .unwrap()
1379 .unwrap_err()
1380 .to_string()
1381 .contains("original wait error"));
1382 }
1383
1384 struct TestEnqueuedTransaction {
1385 transaction: RecorderTransaction<TestTransactionOwner>,
1386 }
1387
1388 impl Drop for TestEnqueuedTransaction {
1389 fn drop(&mut self) {
1390 let _ = self.transaction.abort_with(
1391 |owner| {
1392 owner.events.lock().expect("events").push("sync");
1393 Ok(())
1394 },
1395 |owner, _| {
1396 owner.events.lock().expect("events").push("cancel");
1397 Ok(())
1398 },
1399 );
1400 }
1401 }
1402
1403 fn test_use(ptr: u64) -> BlockUse {
1404 BlockUse {
1405 block: BlockId {
1406 ptr,
1407 generation: Generation(1),
1408 alloc_stream: StreamId(3),
1409 device_ordinal: 0,
1410 },
1411 bytes: 64,
1412 access: Access::Read,
1413 }
1414 }
1415
1416 #[test]
1417 fn submission_admission_rejection_cancels_without_touching_the_stream() {
1418 for cancellation_fails in [false, true] {
1419 let owner = Arc::new(TestTransactionOwner::default());
1420 let mut transaction = RecorderTransaction::from_admitted(
1421 Arc::clone(&owner),
1422 Box::new([test_use(0x1000)]),
1423 );
1424 let result = transaction.enqueue_operation_with(
1425 || Err::<(), _>(ResourceError::StreamMisuse("capture target closed".into())),
1426 |_| panic!("rejected admission must not enqueue dependency waits"),
1427 |_| -> ResourceResult<()> { panic!("rejected admission must not enqueue work") },
1428 |_| panic!("rejected admission must not synchronize a different capture"),
1429 |owner, pending| {
1430 assert_eq!(pending, &[test_use(0x1000)]);
1431 owner.events.lock().unwrap().push("cancel");
1432 if cancellation_fails {
1433 Err(ResourceError::Driver("cancellation failed".into()))
1434 } else {
1435 Ok(())
1436 }
1437 },
1438 );
1439 assert!(matches!(
1440 (&result, cancellation_fails),
1441 (Err(LaunchEnqueueError::Preparation(_)), false)
1442 | (Err(LaunchEnqueueError::PreparationAndCleanup { .. }), true)
1443 ));
1444 assert_eq!(*owner.events.lock().unwrap(), ["cancel"]);
1445 assert!(transaction.is_terminal());
1446 }
1447 }
1448
1449 #[test]
1450 fn submission_admission_guard_covers_prepare_and_operation_but_not_cleanup() {
1451 struct AdmissionGuard(Arc<AtomicBool>);
1452 impl Drop for AdmissionGuard {
1453 fn drop(&mut self) {
1454 assert!(self.0.swap(false, Ordering::SeqCst));
1455 }
1456 }
1457
1458 for outcome in [
1459 "success",
1460 "prepare error",
1461 "operation error",
1462 "prepare panic",
1463 "operation panic",
1464 ] {
1465 let active = Arc::new(AtomicBool::new(false));
1466 let owner = Arc::new(TestTransactionOwner::default());
1467 let mut transaction = RecorderTransaction::from_admitted(
1468 Arc::clone(&owner),
1469 Box::new([test_use(0x1000)]),
1470 );
1471 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1472 transaction.enqueue_operation_with(
1473 || {
1474 assert!(!active.swap(true, Ordering::SeqCst));
1475 Ok(AdmissionGuard(Arc::clone(&active)))
1476 },
1477 |_| {
1478 assert!(active.load(Ordering::SeqCst));
1479 match outcome {
1480 "prepare error" => Err(ResourceError::Driver("prepare failed".into())),
1481 "prepare panic" => panic!("prepare unwound"),
1482 _ => Ok(()),
1483 }
1484 },
1485 |_| {
1486 assert!(active.load(Ordering::SeqCst));
1487 match outcome {
1488 "operation error" => Err("operation failed"),
1489 "operation panic" => panic!("operation unwound"),
1490 _ => Ok(()),
1491 }
1492 },
1493 |_| {
1494 assert!(!active.load(Ordering::SeqCst));
1495 Ok(())
1496 },
1497 |_, _| {
1498 assert!(!active.load(Ordering::SeqCst));
1499 Ok(())
1500 },
1501 )
1502 }));
1503 assert!(!active.load(Ordering::SeqCst));
1504 match outcome {
1505 "success" => assert!(matches!(result, Ok(Ok(())))),
1506 "prepare error" => assert!(matches!(
1507 result,
1508 Ok(Err(LaunchEnqueueError::Preparation(_)))
1509 )),
1510 "operation error" => assert!(matches!(
1511 result,
1512 Ok(Err(LaunchEnqueueError::Operation("operation failed")))
1513 )),
1514 _ => assert!(result.is_err()),
1515 }
1516 if transaction.is_prepared() {
1517 transaction
1518 .abort_with(
1519 |_| {
1520 assert!(!active.load(Ordering::SeqCst));
1521 Ok(())
1522 },
1523 |_, _| Ok(()),
1524 )
1525 .unwrap();
1526 }
1527 }
1528 }
1529
1530 #[test]
1531 fn bound_enqueue_success_marks_nonempty_transaction_before_operation() {
1532 let owner = Arc::new(TestTransactionOwner::default());
1533 let mut transaction =
1534 RecorderTransaction::from_admitted(Arc::clone(&owner), Box::new([test_use(0x1000)]));
1535
1536 let result: Result<(), LaunchEnqueueError<&'static str>> = transaction
1537 .enqueue_operation_with(
1538 || Ok(()),
1539 |owner| {
1540 owner.events.lock().expect("events").push("prepare");
1541 Ok(())
1542 },
1543 |_| {
1544 owner.events.lock().expect("events").push("operation");
1545 Ok(())
1546 },
1547 |_| Ok(()),
1548 |_, _| Ok(()),
1549 );
1550 result.expect("enqueue operation");
1551 transaction
1552 .commit_with(
1553 |owner, _| {
1554 owner.events.lock().expect("events").push("finish");
1555 Ok(())
1556 },
1557 |_| Ok(()),
1558 |_, _| Ok(()),
1559 )
1560 .expect("commit");
1561
1562 assert_eq!(
1563 *owner.events.lock().expect("events"),
1564 ["prepare", "operation", "finish"]
1565 );
1566 }
1567
1568 #[test]
1569 fn bound_enqueue_operation_error_synchronizes_and_cancels() {
1570 let owner = Arc::new(TestTransactionOwner::default());
1571 let mut transaction =
1572 RecorderTransaction::from_admitted(Arc::clone(&owner), Box::new([test_use(0x1000)]));
1573
1574 let error = transaction
1575 .enqueue_operation_with(
1576 || Ok(()),
1577 |_| Ok(()),
1578 |_| {
1579 owner.events.lock().expect("events").push("operation");
1580 Err("injected operation failure")
1581 },
1582 |owner| {
1583 owner.events.lock().expect("events").push("sync");
1584 Ok(())
1585 },
1586 |owner, _| {
1587 owner.events.lock().expect("events").push("cancel");
1588 Ok(())
1589 },
1590 )
1591 .expect_err("operation must fail");
1592
1593 assert!(matches!(
1594 error,
1595 LaunchEnqueueError::Operation("injected operation failure")
1596 ));
1597 assert_eq!(
1598 *owner.events.lock().expect("events"),
1599 ["operation", "sync", "cancel"]
1600 );
1601 assert!(transaction.is_terminal());
1602 }
1603
1604 #[cfg(panic = "unwind")]
1605 #[test]
1606 fn bound_enqueue_operation_panic_drops_through_sync_and_cancel() {
1607 let owner = Arc::new(TestTransactionOwner::default());
1608 let events = Arc::clone(&owner.events);
1609 let transaction =
1610 RecorderTransaction::from_admitted(Arc::clone(&owner), Box::new([test_use(0x1000)]));
1611 drop(owner);
1612 let operation_events = Arc::clone(&events);
1613
1614 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
1615 let mut enqueued = TestEnqueuedTransaction { transaction };
1616 let _result: Result<(), LaunchEnqueueError<&'static str>> =
1617 enqueued.transaction.enqueue_operation_with(
1618 || Ok(()),
1619 |_| Ok(()),
1620 |_| {
1621 operation_events.lock().expect("events").push("operation");
1622 panic!("injected operation panic");
1623 },
1624 |_| Ok(()),
1625 |_, _| Ok(()),
1626 );
1627 }));
1628
1629 assert!(panic.is_err());
1630 assert_eq!(
1631 *events.lock().expect("events"),
1632 ["operation", "sync", "cancel"]
1633 );
1634 }
1635
1636 #[cfg(panic = "unwind")]
1637 struct PanicCleanupDuringUnwind {
1638 transaction: RecorderTransaction<TestTransactionOwner>,
1639 cleanup_panic_caught: Arc<AtomicBool>,
1640 }
1641
1642 #[cfg(panic = "unwind")]
1643 impl Drop for PanicCleanupDuringUnwind {
1644 fn drop(&mut self) {
1645 let cleanup = catch_drop_cleanup(|| {
1646 self.transaction
1647 .abort_with(|_| Ok(()), |_, _| panic!("injected cleanup panic"))
1648 });
1649 self.cleanup_panic_caught
1650 .store(cleanup.is_err(), Ordering::SeqCst);
1651 }
1652 }
1653
1654 #[cfg(panic = "unwind")]
1655 #[test]
1656 fn drop_cleanup_panic_does_not_replace_an_outer_unwind() {
1657 let owner = Arc::new(TestTransactionOwner::default());
1658 let witness = Arc::downgrade(&owner);
1659 let transaction =
1660 RecorderTransaction::from_admitted(Arc::clone(&owner), Box::new([test_use(0x1000)]));
1661 drop(owner);
1662
1663 let cleanup_panic_caught = Arc::new(AtomicBool::new(false));
1664 let outer = std::panic::catch_unwind(std::panic::AssertUnwindSafe({
1665 let cleanup_panic_caught = Arc::clone(&cleanup_panic_caught);
1666 move || {
1667 let _cleanup = PanicCleanupDuringUnwind {
1668 transaction,
1669 cleanup_panic_caught,
1670 };
1671 panic!("outer operation panic");
1672 }
1673 }));
1674
1675 let payload = outer.expect_err("outer operation must still unwind");
1676 assert_eq!(
1677 payload.downcast_ref::<&'static str>(),
1678 Some(&"outer operation panic")
1679 );
1680 assert!(cleanup_panic_caught.load(Ordering::SeqCst));
1681 assert!(
1682 witness.upgrade().is_some(),
1683 "cleanup panic must quarantine the exact transaction owner"
1684 );
1685 }
1686
1687 #[test]
1688 fn dependency_failure_cancels_the_complete_admitted_group() {
1689 let owner = Arc::new(TestTransactionOwner::default());
1690 let uses = Box::new([test_use(0x1000), test_use(0x2000)]);
1691 let mut transaction = RecorderTransaction::from_admitted(Arc::clone(&owner), uses);
1692 let result = transaction.enqueue_operation_with(
1693 || Ok(()),
1694 |_| Err(ResourceError::Driver("injected dependency failure".into())),
1695 |_| -> ResourceResult<()> { panic!("kernel must not run") },
1696 |owner| {
1697 owner.events.lock().unwrap().push("sync");
1698 Ok(())
1699 },
1700 |owner, pending| {
1701 assert_eq!(pending, &[test_use(0x1000), test_use(0x2000)]);
1702 owner.events.lock().unwrap().push("cancel");
1703 Ok(())
1704 },
1705 );
1706 assert!(matches!(result, Err(LaunchEnqueueError::Preparation(_))));
1707 assert_eq!(*owner.events.lock().unwrap(), ["sync", "cancel"]);
1708 assert!(transaction.is_terminal());
1709 }
1710
1711 #[cfg(panic = "unwind")]
1712 #[test]
1713 fn dependency_preparation_panic_keeps_the_armed_owner_for_cleanup() {
1714 let owner = Arc::new(TestTransactionOwner::default());
1715 let witness = Arc::downgrade(&owner);
1716 let mut transaction = RecorderTransaction::from_admitted(
1717 owner,
1718 Box::new([test_use(0x1000), test_use(0x2000)]),
1719 );
1720 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1721 let _: Result<(), LaunchEnqueueError<()>> = transaction.enqueue_operation_with(
1722 || Ok(()),
1723 |_| panic!("injected dependency preparation panic"),
1724 |_| panic!("kernel must not run"),
1725 |_| Ok(()),
1726 |_, _| Ok(()),
1727 );
1728 }));
1729 assert!(panic.is_err());
1730 assert!(witness.upgrade().is_some());
1731 assert!(transaction.is_prepared());
1732 assert!(transaction
1733 .abort_with(
1734 |_| Err(ResourceError::Driver("completion unknown".into())),
1735 |_, _| panic!("unknown completion must not cancel"),
1736 )
1737 .is_err());
1738 assert!(witness.upgrade().is_some());
1739 assert!(transaction.is_terminal());
1740 }
1741
1742 #[cfg(panic = "unwind")]
1743 #[test]
1744 fn abort_callback_panic_quarantines_the_prepared_owner() {
1745 let owner = Arc::new(TestTransactionOwner::default());
1746 let witness = Arc::downgrade(&owner);
1747 let mut transaction =
1748 RecorderTransaction::from_admitted(Arc::clone(&owner), Box::new([test_use(0x1000)]));
1749 drop(owner);
1750
1751 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1752 transaction
1753 .abort_with(|_| Ok(()), |_, _| panic!("injected cancellation panic"))
1754 .expect("abort should panic before returning");
1755 }));
1756
1757 assert!(panic.is_err());
1758 assert!(
1759 witness.upgrade().is_some(),
1760 "abort callback panic must retain the exact owner Arc"
1761 );
1762 assert!(transaction.is_terminal());
1763 }
1764
1765 #[cfg(panic = "unwind")]
1766 #[test]
1767 fn abort_sync_panic_quarantines_the_enqueued_owner() {
1768 let owner = Arc::new(TestTransactionOwner::default());
1769 let witness = Arc::downgrade(&owner);
1770 let mut transaction =
1771 RecorderTransaction::from_admitted(Arc::clone(&owner), Box::new([test_use(0x1000)]));
1772 transaction
1773 .mark_may_have_enqueued()
1774 .expect("enqueue boundary");
1775 drop(owner);
1776
1777 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1778 transaction
1779 .abort_with(|_| panic!("injected synchronization panic"), |_, _| Ok(()))
1780 .expect("abort should panic before returning");
1781 }));
1782
1783 assert!(panic.is_err());
1784 assert!(
1785 witness.upgrade().is_some(),
1786 "abort synchronization panic must retain the exact owner Arc"
1787 );
1788 assert!(transaction.is_terminal());
1789 }
1790
1791 #[cfg(panic = "unwind")]
1792 #[test]
1793 fn commit_callback_panic_quarantines_the_unfinished_owner() {
1794 let owner = Arc::new(TestTransactionOwner::default());
1795 let witness = Arc::downgrade(&owner);
1796 let mut transaction =
1797 RecorderTransaction::from_admitted(Arc::clone(&owner), Box::new([test_use(0x1000)]));
1798 transaction
1799 .mark_may_have_enqueued()
1800 .expect("enqueue boundary");
1801 drop(owner);
1802
1803 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1804 transaction
1805 .commit_with(
1806 |_, _| panic!("injected finish panic"),
1807 |_| Ok(()),
1808 |_, _| Ok(()),
1809 )
1810 .expect("commit should panic before returning");
1811 }));
1812
1813 assert!(panic.is_err());
1814 assert!(
1815 witness.upgrade().is_some(),
1816 "commit callback panic must retain the exact owner Arc"
1817 );
1818 assert!(transaction.is_terminal());
1819 }
1820
1821 #[test]
1822 fn enqueue_cleanup_errors_preserve_both_failures() {
1823 let preparation_error: LaunchEnqueueError<()> = preparation_with_cleanup(
1824 ResourceError::Driver("injected preparation failure".into()),
1825 Err(ResourceError::Driver(
1826 "injected preparation cleanup failure".into(),
1827 )),
1828 );
1829 match preparation_error {
1830 LaunchEnqueueError::PreparationAndCleanup {
1831 preparation,
1832 cleanup,
1833 } => {
1834 assert!(preparation.to_string().contains("preparation failure"));
1835 assert!(cleanup.to_string().contains("preparation cleanup failure"));
1836 }
1837 _ => panic!("preparation cleanup failure lost its structure"),
1838 }
1839
1840 let cleanup = ResourceError::Driver("injected operation cleanup failure".to_string());
1841 let operation_error = operation_with_cleanup("typed operation failure", Err(cleanup));
1842 match operation_error {
1843 LaunchEnqueueError::OperationAndCleanup { operation, cleanup } => {
1844 assert_eq!(operation, "typed operation failure");
1845 assert!(cleanup.to_string().contains("operation cleanup failure"));
1846 }
1847 _ => panic!("operation cleanup failure lost its structure"),
1848 }
1849 }
1850
1851 #[test]
1852 fn dependency_cleanup_failure_quarantines_exact_owner_and_whole_group() {
1853 let owner = Arc::new(TestTransactionOwner::default());
1854 let witness = Arc::downgrade(&owner);
1855 let mut transaction = RecorderTransaction::from_admitted(
1856 owner,
1857 Box::new([test_use(0x1000), test_use(0x2000)]),
1858 );
1859 let result = transaction.enqueue_operation_with(
1860 || Ok(()),
1861 |_| Err(ResourceError::Driver("dependency failure".into())),
1862 |_| -> ResourceResult<()> { panic!("kernel must not run") },
1863 |_| Ok(()),
1864 |_, pending| {
1865 assert_eq!(pending, &[test_use(0x1000), test_use(0x2000)]);
1866 Err(ResourceError::Driver("cancellation failure".into()))
1867 },
1868 );
1869 let error = result.unwrap_err().to_string();
1870 assert!(error.contains("dependency failure"), "{error}");
1871 assert!(error.contains("cancellation failure"), "{error}");
1872 assert!(witness.upgrade().is_some());
1873 assert!(transaction.is_terminal());
1874 }
1875
1876 fn try_async_runtime() -> Option<(Arc<CudaDevice>, Arc<XlogDeviceRuntime>, StreamId)> {
1877 let device = Arc::new(CudaDevice::new(0).ok()?);
1878 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
1879 let async_resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
1880 AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
1881 );
1882 let runtime = Arc::new(XlogDeviceRuntime::with_resource(
1883 Arc::clone(&device),
1884 0,
1885 Arc::clone(&pool),
1886 async_resource,
1887 ));
1888 let launch_stream = pool.acquire().ok()?;
1889 Some((device, runtime, launch_stream))
1890 }
1891
1892 fn try_direct_runtime() -> Option<(Arc<CudaDevice>, Arc<XlogDeviceRuntime>, StreamId)> {
1893 let device = Arc::new(CudaDevice::new(0).ok()?);
1894 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
1895 let direct: Box<dyn DeviceMemoryResource + Send + Sync> =
1896 Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
1897 let runtime = Arc::new(XlogDeviceRuntime::with_resource(
1898 Arc::clone(&device),
1899 0,
1900 Arc::clone(&pool),
1901 direct,
1902 ));
1903 Some((device, runtime, StreamId::DEFAULT))
1904 }
1905
1906 #[test]
1907 fn empty_commit_is_ok_in_both_modes() {
1908 let Some((_d, _rt, ls)) = try_async_runtime() else {
1909 return;
1910 };
1911 LaunchRecorder::new_permissive(ls)
1912 .commit()
1913 .expect("permissive empty");
1914 LaunchRecorder::new_strict(ls)
1915 .commit()
1916 .expect("strict empty");
1917 }
1918
1919 #[test]
1920 fn permissive_records_native_storage_without_a_runtime_block() {
1921 let Some(device) = CudaDevice::new(0).ok().map(Arc::new) else {
1922 return;
1923 };
1924 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
1925 let async_resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
1926 AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
1927 );
1928 let runtime = Arc::new(XlogDeviceRuntime::with_resource(
1929 Arc::clone(&device),
1930 0,
1931 Arc::clone(&pool),
1932 async_resource,
1933 ));
1934 let launch_stream = pool.acquire().expect("acquire");
1935
1936 let manager = Arc::new(crate::GpuMemoryManager::new(
1938 Arc::clone(&device),
1939 MemoryBudget::with_limit(1024 * 1024),
1940 ));
1941 let native = manager.alloc::<u8>(64).expect("native alloc");
1942 assert!(native.runtime_block().is_none());
1943
1944 let mut rec = LaunchRecorder::new_permissive(launch_stream);
1945 rec.read(&native);
1946 assert_eq!(rec.recorded_count(), 1);
1947 rec.preflight(&runtime).expect("permissive preflight");
1948 let stream = runtime.stream_pool().resolve(launch_stream).unwrap();
1949 let rec = unsafe { rec.enqueue_prepared_with(&stream, |_| Ok::<_, ResourceError>(())) }
1951 .expect("prepare dependencies");
1952 rec.commit().expect("permissive commit");
1953 }
1954
1955 #[test]
1956 fn strict_retains_native_storage_after_the_callers_handle_is_dropped() {
1957 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
1958 return;
1959 };
1960 let manager = Arc::new(crate::GpuMemoryManager::new(
1961 Arc::clone(&device),
1962 MemoryBudget::with_limit(1024 * 1024),
1963 ));
1964 let native = manager.alloc::<u8>(64).expect("native alloc");
1965
1966 let mut rec = LaunchRecorder::new_strict(launch_stream);
1967 rec.read(&native);
1968 rec.preflight(&runtime).expect("strict native preflight");
1969 drop(native);
1970 assert_eq!(manager.allocated_bytes(), 64);
1971 rec.abort().expect("cancel before enqueue");
1972 assert_eq!(manager.allocated_bytes(), 0);
1973 }
1974
1975 #[test]
1976 fn preflight_accepts_owned_storage_from_a_direct_runtime() {
1977 let Some((device, runtime, launch_stream)) = try_direct_runtime() else {
1978 return;
1979 };
1980 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
1981 Arc::clone(&device),
1982 MemoryBudget::with_limit(1024 * 1024),
1983 Arc::clone(&runtime),
1984 ));
1985 let buf = manager.alloc::<u8>(64).expect("alloc");
1986 assert!(buf.runtime_block().is_some());
1987
1988 let mut rec = LaunchRecorder::new_strict(launch_stream);
1989 rec.read(&buf);
1990 rec.preflight(&runtime).expect("direct storage preflight");
1991 let stream = runtime.stream_pool().resolve(launch_stream).unwrap();
1992 let rec = unsafe { rec.enqueue_prepared_with(&stream, |_| Ok::<_, ResourceError>(())) }
1994 .expect("prepare dependencies");
1995 rec.commit().expect("publish storage completion");
1996 }
1997
1998 #[test]
1999 fn preflight_then_commit_async_runtime() {
2000 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
2001 return;
2002 };
2003 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
2004 Arc::clone(&device),
2005 MemoryBudget::with_limit(1024 * 1024),
2006 Arc::clone(&runtime),
2007 ));
2008 let buf = manager.alloc::<u8>(64).expect("alloc");
2009
2010 let mut rec = LaunchRecorder::new_strict(launch_stream);
2011 rec.read(&buf);
2012 rec.preflight(&runtime).expect("preflight ok");
2013 let stream = runtime.stream_pool().resolve(launch_stream).unwrap();
2014 let rec = unsafe { rec.enqueue_prepared_with(&stream, |_| Ok::<_, ResourceError>(())) }
2016 .expect("enqueue boundary");
2017 rec.commit().expect("commit ok");
2018 }
2019
2020 #[test]
2021 fn commit_rejects_un_preflighted_strict_recorder() {
2022 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
2023 return;
2024 };
2025 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
2026 Arc::clone(&device),
2027 MemoryBudget::with_limit(1024 * 1024),
2028 Arc::clone(&runtime),
2029 ));
2030 let buf = manager.alloc::<u8>(64).expect("alloc");
2031
2032 let mut rec = LaunchRecorder::new_strict(launch_stream);
2033 rec.read(&buf);
2034 let err = rec.commit();
2035 match err {
2036 Err(ResourceError::StreamMisuse(msg)) => {
2037 assert!(
2038 msg.contains("without a successful preflight"),
2039 "msg: {}",
2040 msg
2041 );
2042 }
2043 other => panic!(
2044 "non-empty un-preflighted commit must return StreamMisuse, got {:?}",
2045 other
2046 ),
2047 }
2048 }
2049
2050 #[test]
2051 fn empty_recorder_commit_without_preflight_is_ok() {
2052 let Some((_d, _rt, ls)) = try_async_runtime() else {
2053 return;
2054 };
2055 LaunchRecorder::new_strict(ls)
2056 .commit()
2057 .expect("empty strict commit without preflight");
2058 }
2059
2060 #[test]
2061 fn note_after_preflight_via_standard_method_is_rejected() {
2062 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
2063 return;
2064 };
2065 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
2066 Arc::clone(&device),
2067 MemoryBudget::with_limit(1024 * 1024),
2068 Arc::clone(&runtime),
2069 ));
2070 let buf_a = manager.alloc::<u8>(64).expect("alloc a");
2071 let buf_b = manager.alloc::<u8>(64).expect("alloc b");
2072
2073 let mut rec = LaunchRecorder::new_strict(launch_stream);
2074 rec.read(&buf_a);
2075 rec.preflight(&runtime).expect("preflight ok");
2076 rec.read(&buf_b);
2077 let err = rec.commit();
2078 match err {
2079 Err(ResourceError::StreamMisuse(msg)) => {
2080 assert!(msg.contains("recorded after preflight"), "msg: {}", msg);
2081 }
2082 other => panic!(
2083 "post-preflight standard-method record must be rejected; got {:?}",
2084 other
2085 ),
2086 }
2087 }
2088
2089 #[test]
2094 fn pre_preflight_fresh_write_is_accepted() {
2095 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
2096 return;
2097 };
2098 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
2099 Arc::clone(&device),
2100 MemoryBudget::with_limit(1024 * 1024),
2101 Arc::clone(&runtime),
2102 ));
2103 let buf_a = manager.alloc::<u8>(64).expect("alloc a");
2104 let mut buf_fresh = manager.alloc::<u8>(64).expect("alloc fresh");
2105
2106 let mut rec = LaunchRecorder::new_strict(launch_stream);
2107 rec.read(&buf_a);
2108 rec.write(&buf_fresh);
2109 rec.preflight(&runtime).expect("preflight ok");
2110 let _kernel_param = &mut buf_fresh;
2112 let stream = runtime.stream_pool().resolve(launch_stream).unwrap();
2113 let rec = unsafe { rec.enqueue_prepared_with(&stream, |_| Ok::<_, ResourceError>(())) }
2115 .expect("enqueue boundary");
2116 rec.commit().expect("commit ok");
2117 }
2118
2119 #[test]
2122 fn read_then_write_same_block_dedupes_to_read_write() {
2123 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
2124 return;
2125 };
2126 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
2127 Arc::clone(&device),
2128 MemoryBudget::with_limit(1024 * 1024),
2129 Arc::clone(&runtime),
2130 ));
2131 let buf = manager.alloc::<u8>(64).expect("alloc");
2132
2133 let mut rec = LaunchRecorder::new_strict(launch_stream);
2134 rec.read(&buf);
2135 rec.write(&buf);
2136 rec.preflight(&runtime).expect("preflight");
2137 let stream = runtime.stream_pool().resolve(launch_stream).unwrap();
2138 let rec = unsafe { rec.enqueue_prepared_with(&stream, |_| Ok::<_, ResourceError>(())) }
2140 .expect("enqueue boundary");
2141 rec.commit().expect("commit");
2142 }
2143
2144 #[test]
2151 fn dedup_keys_on_full_block_id_not_ptr_alone() {
2152 let block_a = BlockId {
2157 ptr: 0xdead_beef,
2158 generation: Generation(1),
2159 alloc_stream: StreamId::DEFAULT,
2160 device_ordinal: 0,
2161 };
2162 let block_b = BlockId {
2163 ptr: 0xdead_beef,
2164 generation: Generation(2),
2165 alloc_stream: StreamId::DEFAULT,
2166 device_ordinal: 0,
2167 };
2168 let uses = vec![
2169 BlockUse {
2170 block: block_a,
2171 bytes: 64,
2172 access: Access::Read,
2173 },
2174 BlockUse {
2175 block: block_b,
2176 bytes: 64,
2177 access: Access::Write,
2178 },
2179 ];
2180 let deduped = dedup_uses(&uses);
2181 assert_eq!(deduped.len(), 2, "ABA generations must NOT collapse");
2182 assert_eq!(deduped[0].block.generation, Generation(1));
2183 assert_eq!(deduped[0].access, Access::Read);
2184 assert_eq!(deduped[1].block.generation, Generation(2));
2185 assert_eq!(deduped[1].access, Access::Write);
2186
2187 let same_id = vec![
2190 BlockUse {
2191 block: block_a,
2192 bytes: 64,
2193 access: Access::Read,
2194 },
2195 BlockUse {
2196 block: block_a,
2197 bytes: 64,
2198 access: Access::Write,
2199 },
2200 ];
2201 let collapsed = dedup_uses(&same_id);
2202 assert_eq!(collapsed.len(), 1);
2203 assert_eq!(collapsed[0].access, Access::ReadWrite);
2204 }
2205
2206 #[test]
2207 fn dedup_distinguishes_allocation_stream_in_full_block_identity() {
2208 let block_a = BlockId {
2209 ptr: 0xdead_beef,
2210 generation: Generation(1),
2211 alloc_stream: StreamId(7),
2212 device_ordinal: 0,
2213 };
2214 let block_b = BlockId {
2215 alloc_stream: StreamId(11),
2216 ..block_a
2217 };
2218 let uses = vec![
2219 BlockUse {
2220 block: block_a,
2221 bytes: 64,
2222 access: Access::Read,
2223 },
2224 BlockUse {
2225 block: block_b,
2226 bytes: 64,
2227 access: Access::Write,
2228 },
2229 ];
2230
2231 let deduped = dedup_uses(&uses);
2232 assert_eq!(deduped.len(), 2, "allocation streams are part of BlockId");
2233 assert_eq!(deduped[0].block.alloc_stream, StreamId(7));
2234 assert_eq!(deduped[1].block.alloc_stream, StreamId(11));
2235 }
2236
2237 #[test]
2238 fn read_device_block_snapshots_complete_identity_as_read() {
2239 let block = DeviceBlock {
2240 ptr: 0x1234,
2241 device_ordinal: 2,
2242 alloc_stream: StreamId(5),
2243 bytes: 64,
2244 align: 16,
2245 tag: crate::device_runtime::AllocTag::UNTAGGED,
2246 generation: Generation(9),
2247 state: crate::device_runtime::BlockState::Live,
2248 };
2249 let expected = BlockId::from_block(&block);
2250 let mut recorder = LaunchRecorder::new_strict(StreamId(8));
2251
2252 recorder.read_device_block(&block);
2253
2254 assert_eq!(recorder.uses.len(), 1);
2255 assert_eq!(recorder.uses[0].block, expected);
2256 assert_eq!(recorder.uses[0].bytes, block.bytes);
2257 assert_eq!(recorder.uses[0].access, Access::Read);
2258 recorder.transaction.mark_terminal();
2259 }
2260
2261 #[test]
2262 fn read_block_identity_records_prevalidated_receipt_pointee() {
2263 let identity = BlockId {
2264 ptr: 0x9876,
2265 generation: Generation(12),
2266 alloc_stream: StreamId(4),
2267 device_ordinal: 3,
2268 };
2269 let mut recorder = LaunchRecorder::new_strict(StreamId(6));
2270
2271 recorder.read_block_identity((identity, 128));
2272
2273 assert_eq!(recorder.uses.len(), 1);
2274 assert_eq!(recorder.uses[0].block, identity);
2275 assert_eq!(recorder.uses[0].bytes, 128);
2276 assert_eq!(recorder.uses[0].access, Access::Read);
2277 recorder.transaction.mark_terminal();
2278 }
2279
2280 #[test]
2281 fn bound_identity_uses_arc_ownership_not_value_equality() {
2282 let owner = Arc::new(());
2283 let same_owner = Arc::clone(&owner);
2284 let equal_value_foreign_owner = Arc::new(());
2285
2286 assert!(same_arc_identity(Some(&owner), &same_owner));
2287 assert!(!same_arc_identity(Some(&owner), &equal_value_foreign_owner));
2288 assert!(!same_arc_identity::<()>(None, &owner));
2289 }
2290
2291 #[test]
2292 fn read_column_owned_runtime_backed() {
2293 use crate::memory::CudaColumn;
2294 let Some((device, runtime, launch_stream)) = try_async_runtime() else {
2295 return;
2296 };
2297 let manager = Arc::new(crate::GpuMemoryManager::with_runtime(
2298 Arc::clone(&device),
2299 MemoryBudget::with_limit(1024 * 1024),
2300 Arc::clone(&runtime),
2301 ));
2302 let slice = manager.alloc::<u8>(64).expect("alloc");
2303 let col = CudaColumn::owned(slice);
2304 assert!(col.runtime_block().is_some());
2305
2306 let mut rec = LaunchRecorder::new_strict(launch_stream);
2307 rec.read_column(&col);
2308 assert_eq!(rec.recorded_count(), 1);
2309 rec.preflight(&runtime).expect("preflight");
2310 let stream = runtime.stream_pool().resolve(launch_stream).unwrap();
2311 let rec = unsafe { rec.enqueue_prepared_with(&stream, |_| Ok::<_, ResourceError>(())) }
2313 .expect("enqueue boundary");
2314 rec.commit().expect("commit");
2315 }
2316}