Skip to main content

xlog_cuda/
launch.rs

1//! Owner-bearing access recorder for one CUDA launch.
2//!
3//! The recorder retains real allocations/import tokens without retaining Rust
4//! borrows, so callers can subsequently build mutable kernel parameters.
5//! Exact CUDA context and byte spans participate in the same atomic admission
6//! as safe device copies, including independently imported overlapping views.
7//! Runtime-only block identities are validated against the owning allocator.
8//!
9//! `enqueue` admits the whole manifest, arms cleanup, queues dependency waits,
10//! and invokes one synchronous enqueue closure on the resolved stream. Commit
11//! publishes every shared dependency before releasing the reservation. Failure
12//! or unwind synchronizes possible work; unknown completion retains the actual
13//! owners and the whole reservation.
14//!
15//! Both modes retain owner-bearing sources. Strict mode is additionally required
16//! by sealed resident execution domains. Neither mode permits arbitrary writes
17//! by an external framework: unsafe import must establish the producer handoff,
18//! and subsequent foreign access still requires coordination with that producer.
19
20use 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/// Recorder construction mode.
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum RecorderMode {
39    /// Unbound execution domain; all registered owners participate in admission.
40    Permissive,
41    /// Eligible for a sealed resident execution domain.
42    Strict,
43}
44
45/// Failure while preparing, enqueueing, or cleaning up a recorded operation.
46#[non_exhaustive]
47#[derive(Debug)]
48pub enum LaunchEnqueueError<E> {
49    /// The recorder could not prepare the operation for enqueue.
50    Preparation(ResourceError),
51    /// Preparation was rejected and phase-aware cleanup also failed.
52    PreparationAndCleanup {
53        preparation: ResourceError,
54        cleanup: ResourceError,
55    },
56    /// The caller's operation returned its typed error after preparation.
57    Operation(E),
58    /// The operation failed and phase-aware cleanup also failed.
59    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
128/// Records buffer uses for a single launch or copy on `launch_stream`.
129/// Consume it with [`Self::enqueue`], then consume the returned
130/// [`EnqueuedLaunch`] with `commit` or `abort`. Dropping a prepared
131/// transaction automatically aborts it and never panics.
132///
133/// # Lifetime model
134///
135/// Registration clones the actual backing owner, not the device bytes, and
136/// immediately releases the source borrow. Callers can subsequently borrow
137/// mutable kernel parameters. Internal bare block identities are validated
138/// against the retained runtime's allocator during atomic admission.
139///
140/// # Required public call order for non-empty recorders
141///
142/// [`Self::enqueue`] MUST own the recorder and the closure MUST
143/// enqueue every operation synchronously on its supplied stream.
144/// Preparation queues the cross-stream waits each recorded access
145/// kind requires (read waits on prior writes; write waits on prior
146/// writes and prior reads), so the operation sees a well-fenced view
147/// of every input. [`EnqueuedLaunch::commit`] then records the new
148/// event on `launch_stream` so future operations can wait on it.
149///
150/// During managed stream capture, the same registration transfers passive
151/// allocation owners and access ranges to the graph without recording events.
152/// Each replay admits its complete manifest before submitting device work.
153/// Nested kernels and graphs use the supplied [`CudaEnqueue`] through their
154/// `launch_in` methods, without preparing another recorder inside the callback.
155///
156/// Empty recorders (no `read`/`write`/... calls) are a no-op
157/// and bypass runtime preparation: there are no waits
158/// to queue, no events to record.
159pub struct LaunchRecorder {
160    launch_stream: StreamId,
161    mode: RecorderMode,
162    /// Recorded uses, snapshotted from source blocks at record
163    /// time. The recorder holds no slice borrows after the
164    /// record call returns — `&mut` kernel params are free.
165    uses: Vec<BlockUse>,
166    accesses: Vec<DeviceMemoryAccess>,
167    /// First strict-mode rejection encountered while recording.
168    /// Surfaced from `preflight`; the recorder's record methods
169    /// return `&mut Self` so callers can chain naturally.
170    strict_reject: Option<ResourceError>,
171    transaction: RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>,
172    // Release host capture admission even when storage enters quarantine.
173    submission: Option<crate::cuda_graph::StreamSubmissionPhase>,
174    bound_runtime: Option<Arc<XlogDeviceRuntime>>,
175    bound_domain: Option<Arc<()>>,
176}
177
178/// Borrowed authority for one already-admitted enqueue. Nested kernel and graph
179/// submissions use this proof rather than re-entering capture or memory admission.
180/// It cannot escape the callback or be reconstructed from a CUDA stream handle.
181pub 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/// Enqueued operation that must be completed or cancelled exactly once.
226#[must_use = "an enqueued launch must be committed or aborted"]
227pub struct EnqueuedLaunch {
228    recorder: LaunchRecorder,
229}
230
231impl EnqueuedLaunch {
232    /// Publish the recorded uses after the operation was enqueued.
233    pub fn commit(self) -> ResourceResult<()> {
234        self.recorder.commit()
235    }
236
237    /// Synchronize possible work and cancel the prepared reservations.
238    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
253/// Cleanup remains attached to the admitted owner after its caller has gone.
254pub(crate) trait RecorderCleanup<U>: Send + Sync + 'static {
255    /// Prove this closed admission complete without reinterpreting a foreign
256    /// thread's implicit stream. Called only by the cold retirement queue.
257    fn synchronize_retired(&self) -> ResourceResult<()>;
258
259    /// Release one exact reservation. Error or unwind must not consume it;
260    /// successful cancellation is recorded before attempting the next one.
261    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        // Transfer the original capsule, without allocating while unwinding.
322        (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    /// Take the exact owner and reservations immediately after atomic admission.
338    /// The caller must install this in its cleanup owner before fallible work.
339    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        // Dependency waits can themselves enqueue work. The owner and every
398        // reservation must already be armed when a wait fails or unwinds.
399        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        // Validate and pin the exact submission phase before any dependency
421        // wait can touch the stream. A stale phase has enqueued nothing and
422        // must cancel without synchronizing a potentially different capture.
423        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        // Capture close may now proceed. Never hold its executing pin over
441        // cleanup synchronization; unwinding also drops this local guard.
442        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
570/// Preserve the exact owner and unfinished reservations until cold cleanup can
571/// prove completion. The authoritative queue retains this same capsule on error
572/// or unwind; no launch, successful cancellation, or completed wait is repeated.
573fn 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    // Enqueue may eagerly drain. Its unwind restores the callable owner to the
581    // queue, but must not replace the caller's original error or outer panic.
582    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        // An arbitrary secondary panic payload can itself panic in Drop. Its
605        // diagnostic value cannot justify replacing the primary failure; the
606        // actual resource capsule is already restored to the retirement queue.
607        std::mem::forget(payload);
608    }
609}
610
611impl LaunchRecorder {
612    /// Recorder for an unbound execution domain. Owner-bearing inputs are retained.
613    pub fn new_permissive(launch_stream: StreamId) -> Self {
614        Self::new(launch_stream, RecorderMode::Permissive)
615    }
616
617    /// Recorder eligible for a sealed resident execution domain.
618    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    /// Configured launch stream.
648    pub fn launch_stream(&self) -> StreamId {
649        self.launch_stream
650    }
651
652    /// Configured mode.
653    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    /// Retain the actual allocation/import owner and record its exact read span.
727    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    /// Record a read through an already-validated runtime block.
735    ///
736    /// Crate-internal owner capsules use this when a device pointer table
737    /// retains immutable host-side block identities instead of the typed
738    /// slices that originally supplied the pointees.
739    #[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    /// Record a read through a prevalidated immutable block-identity snapshot.
752    #[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    /// Retain the actual owner and record the exact writable span, including
765    /// fresh output allocations and independently imported aliases.
766    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    /// Retain an owner-bearing span used for both reading and writing.
774    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    /// Record a column read and retain its allocation or external import token.
782    pub fn read_column(&mut self, col: &crate::memory::CudaColumn) -> &mut Self {
783        self.read(col)
784    }
785
786    /// Record a [`crate::memory::CudaColumn`] the launch will
787    /// write.
788    pub fn write_column(&mut self, col: &crate::memory::CudaColumn) -> &mut Self {
789        self.write(col)
790    }
791
792    /// Number of recorded registrations before deduplication. Diagnostic.
793    pub fn recorded_count(&self) -> usize {
794        self.uses.len() + self.accesses.len()
795    }
796
797    /// Atomically admit all recorded spans and retain their actual owners.
798    /// This stage queues no driver work. The enqueue boundary installs required
799    /// dependency waits after arming the transaction for failure and unwind.
800    /// Repeated calls are rejected without discarding an existing reservation.
801    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    /// Prepare the recorded uses and synchronously invoke one enqueue operation.
848    ///
849    /// # Safety
850    ///
851    /// `operation` must enqueue all work synchronously before it returns and use
852    /// only the supplied enqueue capability. Every XLOG-owned allocation it
853    /// touches must be registered on this recorder with its exact access mode.
854    /// Nested operations must reuse that capability rather than admitting
855    /// another recorder or submitting independently on its raw stream.
856    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        // SAFETY: this helper carries the same caller obligations as `enqueue`.
890        unsafe { self.enqueue_prepared_with(stream.as_ref(), operation) }
891    }
892
893    /// Consume a recorder whose runtime, domain marker, and stream were sealed together.
894    ///
895    /// # Safety
896    ///
897    /// `operation` must satisfy the same synchronous enqueue, stream identity,
898    /// allocation registration, and enclosing-transaction obligations as
899    /// [`Self::enqueue`].
900    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        // SAFETY: this helper carries the same caller obligations as `enqueue_bound`.
923        unsafe { self.enqueue_prepared_with(stream, operation) }
924    }
925
926    /// Enqueue using the exact admission and stream selected by preflight.
927    ///
928    /// # Safety
929    /// `stream` must be the stream admitted by this recorder, and `operation`
930    /// must satisfy the synchronous enqueue and access obligations of `enqueue`.
931    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    /// Cancel this launch transaction using the runtime saved by preflight.
995    pub(crate) fn abort(mut self) -> ResourceResult<()> {
996        self.abort_transaction()
997    }
998
999    /// Commit the recorded uses to the runtime. MUST be called
1000    /// AFTER preflight succeeded AND the CUDA launch has been
1001    /// enqueued on `launch_stream`.
1002    ///
1003    /// **Non-empty recorders that were not preflighted are
1004    /// rejected** with `StreamMisuse`. This closes the footgun
1005    /// where a caller could enqueue CUDA work, then call
1006    /// commit, then discover at commit-time that the active
1007    /// resource is unsupported — leaving unprotected work in
1008    /// flight. Production migrated launch paths must therefore
1009    /// always preflight BEFORE the CUDA call.
1010    ///
1011    /// Empty recorders (no recorded uses) bypass the check:
1012    /// nothing to record, no events to fire, no contract to
1013    /// honor.
1014    ///
1015    /// Records completion in every retained shared dependency owner before
1016    /// releasing the one admission group. Only the recording stream's frontier
1017    /// is replaced; other streams' concurrent operations remain visible.
1018    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
1084/// Collapse multiple registrations of the same block into one
1085/// use with the strongest access.
1086///
1087/// The dedup key is the complete [`BlockId`] identity
1088/// `(ptr, generation, alloc_stream, device_ordinal)` — NOT
1089/// `ptr` alone. ABA reuse inside a single recorder is rare but
1090/// possible (record use of buffer X, drop X, allocate a new
1091/// block reusing X's address, record THAT) and a ptr-only key
1092/// would incorrectly merge those two distinct uses. Keying by
1093/// the full identity tuple lets the prepare/finish path see
1094/// each generation's events independently; the runtime's
1095/// generation guard then catches any stale id at the resource
1096/// boundary.
1097///
1098/// Access combine: Read+Write → ReadWrite; otherwise the
1099/// strongest of the two operands wins.
1100fn 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
1125/// Strongest-access lattice: ReadWrite >= Write/Read; Write+Read = ReadWrite.
1126pub(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        // Clean the queued owner even on the failing side of this regression.
1370        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        // A native allocation remains owned even without a runtime block.
1937        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        // SAFETY: the callback enqueues no additional work.
1950        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        // SAFETY: the callback enqueues no additional work.
1993        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        // SAFETY: the callback enqueues no additional work.
2015        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    /// Pre-launch fresh-write path: fresh outputs are recorded
2090    /// BEFORE preflight via the regular `write` API. Snapshot
2091    /// drops the source borrow, so kernel `&mut` borrows after
2092    /// preflight remain valid.
2093    #[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        // Borrows are released; kernel-style &mut works here.
2111        let _kernel_param = &mut buf_fresh;
2112        let stream = runtime.stream_pool().resolve(launch_stream).unwrap();
2113        // SAFETY: the callback enqueues no additional work.
2114        let rec = unsafe { rec.enqueue_prepared_with(&stream, |_| Ok::<_, ResourceError>(())) }
2115            .expect("enqueue boundary");
2116        rec.commit().expect("commit ok");
2117    }
2118
2119    /// Read+write of the same block in a single recorder
2120    /// dedupes to a single ReadWrite prepare/finish call.
2121    #[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        // SAFETY: the callback enqueues no additional work.
2139        let rec = unsafe { rec.enqueue_prepared_with(&stream, |_| Ok::<_, ResourceError>(())) }
2140            .expect("enqueue boundary");
2141        rec.commit().expect("commit");
2142    }
2143
2144    /// Locks the dedup key: `(ptr, generation, device_ordinal)`,
2145    /// not `ptr` alone. Two `BlockUse`s sharing a ptr but
2146    /// differing in generation MUST be treated as distinct
2147    /// entries — otherwise an ABA reuse inside a single recorder
2148    /// would silently collapse an event for the new allocation
2149    /// onto the old block's prepare/finish chain.
2150    #[test]
2151    fn dedup_keys_on_full_block_id_not_ptr_alone() {
2152        // Construct two block uses with the same ptr but
2153        // distinct generations — directly drive `dedup_uses` so
2154        // the test is deterministic and does not require ABA to
2155        // actually occur on real CUDA.
2156        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        // Same ptr + same generation + duplicate access must
2188        // collapse into one entry with combined access.
2189        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        // SAFETY: the callback enqueues no additional work.
2312        let rec = unsafe { rec.enqueue_prepared_with(&stream, |_| Ok::<_, ResourceError>(())) }
2313            .expect("enqueue boundary");
2314        rec.commit().expect("commit");
2315    }
2316}