Skip to main content

xlog_cuda/
memory.rs

1//! CUDA memory management
2//!
3//! This module provides GPU memory management with budget enforcement.
4//! It wraps cudarc's allocation functions and tracks total allocated memory.
5
6use std::mem::ManuallyDrop;
7use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
8use std::sync::Arc;
9
10use cudarc::driver::{CudaStream, DevicePtr, DevicePtrMut, DeviceRepr, DeviceSlice, SyncOnDrop};
11use xlog_core::{resolve_bool, MemoryBudget, Result, Schema, XlogError};
12
13use crate::arrow_device::ArrowDeviceImport;
14use crate::cuda_compat::{AsKernelParam, DeviceParamStorage, IntoKernelParamStorage};
15use crate::device_runtime::resource::{
16    block_use_registry, Access, AllocationAccounting, AllocationRequest, BlockUseRegistry,
17    DeviceAccessDependencies, MemoryStorageOwner, MemoryUse, MemoryUseGroup, RetainedStorageUse,
18};
19use crate::device_runtime::{
20    AllocTag, BlockId, BlockState, DeviceBlock, ResourceError, RuntimeMemoryReservation, StreamId,
21    XlogDeviceRuntime,
22};
23use crate::dlpack::DlpackManagedTensor;
24use crate::launch::RecorderTransaction;
25use crate::CudaDevice;
26
27#[cfg(test)]
28type AfterLocalReservationHook = std::sync::Mutex<Option<Arc<dyn Fn(u64) + Send + Sync + 'static>>>;
29
30/// Budget-enforced device allocation. Every view retains actual storage.
31/// Runtime-backed managers use the canonical resource stack. Native allocations
32/// share its raw owner: post-malloc failures retain storage and budget together.
33pub struct GpuMemoryManager {
34    /// The CUDA device for memory operations
35    device: Arc<CudaDevice>,
36    /// Memory budget configuration
37    budget: MemoryBudget,
38    /// Accounting shared by every allocation view over this budget.
39    accounting: Arc<GpuMemoryAccounting>,
40    /// Optional v0.6 device runtime. When set, [`alloc_raw`]
41    /// reserves through the runtime's resource stack in addition
42    /// to enforcing the local budget; both must accept for the
43    /// allocation to proceed.
44    runtime: Option<Arc<XlogDeviceRuntime>>,
45    /// Unit-test seam used to pause a request after local reservation but
46    /// before runtime admission. Production builds contain no hook.
47    #[cfg(test)]
48    after_local_reservation_hook: AfterLocalReservationHook,
49}
50
51#[derive(Default)]
52struct GpuMemoryAccounting {
53    /// Serializes accounting mutations that must validate multiple counters
54    /// before changing any of them.
55    mutation_lock: std::sync::Mutex<()>,
56    /// Bytes reserved against the local budget, including requests that have
57    /// passed the local guard but are still awaiting allocator admission.
58    /// This counter is intentionally conservative so concurrent requests
59    /// cannot oversubscribe the configured budget.
60    budget_reserved: AtomicU64,
61    /// Currently admitted bytes (tracked atomically for thread safety).
62    /// Unlike `budget_reserved`, this excludes provisional and refused
63    /// requests and is the value exposed by [`allocated_bytes`](Self::allocated_bytes).
64    allocated: AtomicU64,
65    /// High-water mark of successful manager-accounted reservations since
66    /// construction or the last [`reset_peak`](Self::reset_peak). This is a
67    /// reservation-lifetime metric, not a physical-memory measurement.
68    peak: AtomicU64,
69    /// Count of `alloc` calls (device allocation requests). Resettable; used by
70    /// the GPU-resident MC engine's no-host gate to prove that **zero** device
71    /// allocations happen inside the measured region (all arenas are allocated
72    /// before it). Distinct from `allocated` (bytes).
73    alloc_count: AtomicU64,
74    /// Runtime deallocations that returned an error. Their bytes remain
75    /// charged locally because physical release was not proven.
76    deallocation_failure_count: AtomicU64,
77    deallocation_failure_bytes: AtomicU64,
78}
79
80impl GpuMemoryAccounting {
81    fn release_reserved(&self, bytes: u64) -> Result<()> {
82        let _mutation = self.mutation_lock.lock().map_err(|_| {
83            XlogError::Kernel("GPU memory accounting poisoned during reservation release".into())
84        })?;
85        let previous = self.budget_reserved.load(Ordering::SeqCst);
86        let next = previous.checked_sub(bytes).ok_or_else(|| {
87            XlogError::Kernel(format!(
88                "GPU memory reservation release underflow: current_bytes={} requested_bytes={}",
89                previous, bytes,
90            ))
91        })?;
92        self.budget_reserved.store(next, Ordering::SeqCst);
93        Ok(())
94    }
95
96    fn release_owned_allocation(&self, bytes: u64) -> Result<()> {
97        let _mutation = self.mutation_lock.lock().map_err(|_| {
98            XlogError::Kernel("GPU memory accounting poisoned during release".into())
99        })?;
100        let admitted = self.allocated.load(Ordering::SeqCst);
101        let reserved = self.budget_reserved.load(Ordering::SeqCst);
102        let next_admitted = admitted.checked_sub(bytes).ok_or_else(|| {
103            XlogError::Kernel(format!(
104                "GPU admitted allocation release underflow: current_bytes={} requested_bytes={}",
105                admitted, bytes
106            ))
107        })?;
108        let next_reserved = reserved.checked_sub(bytes).ok_or_else(|| {
109            XlogError::Kernel(format!(
110                "GPU local reservation release underflow: current_bytes={} requested_bytes={}",
111                reserved, bytes
112            ))
113        })?;
114        self.allocated.store(next_admitted, Ordering::SeqCst);
115        self.budget_reserved.store(next_reserved, Ordering::SeqCst);
116        Ok(())
117    }
118}
119
120/// Accounting owned by one allocation attempt and then by its physical storage.
121///
122/// Resource implementations bind their accounting before malloc, call
123/// [`Self::acquired`] immediately after arming the returned pointer, and retain
124/// this owner until [`Self::confirm_physical_release`] settles physical release.
125/// Refusal and unwind guards use the persistent acquisition proof, not the
126/// shape of an error or whether a concurrent reaper already freed the storage.
127#[derive(Default)]
128pub struct AllocationReclamation {
129    state: std::sync::Mutex<AllocationReclamationState>,
130    acquired: std::sync::atomic::AtomicBool,
131    released: Arc<std::sync::atomic::AtomicBool>,
132}
133
134#[derive(Default)]
135struct AllocationReclamationState {
136    charge: Option<LocalAllocationCharge>,
137    local_published: bool,
138    resource: Option<(Arc<AllocationAccounting>, usize)>,
139    resource_published: bool,
140    pending: Option<(Arc<std::sync::atomic::AtomicUsize>, usize)>,
141}
142
143struct LocalAllocationCharge {
144    accounting: Arc<GpuMemoryAccounting>,
145    bytes: u64,
146}
147
148impl AllocationReclamation {
149    /// Transfer an already reserved local claim before any physical allocation.
150    fn attach_local(
151        &self,
152        accounting: Arc<GpuMemoryAccounting>,
153        bytes: u64,
154    ) -> crate::device_runtime::ResourceResult<()> {
155        let charge = LocalAllocationCharge { accounting, bytes };
156        let mut state = self.state.lock().map_err(|_| {
157            ResourceError::Driver(
158                "allocation reclamation state poisoned during charge transfer".into(),
159            )
160        })?;
161        if self.was_acquired() || self.was_released() || state.charge.is_some() {
162            return Err(ResourceError::Driver(
163                "allocation charge transfer requires an unreleased, unbound owner".into(),
164            ));
165        }
166        state.charge = Some(charge);
167        Ok(())
168    }
169
170    /// Bind the backend's exact accounting domain before calling malloc.
171    pub fn attach_resource(
172        &self,
173        accounting: Arc<AllocationAccounting>,
174        bytes: usize,
175    ) -> crate::device_runtime::ResourceResult<()> {
176        let mut state = self.state.lock().map_err(|_| {
177            ResourceError::Driver("allocation accounting poisoned during binding".into())
178        })?;
179        if self.was_acquired() || self.was_released() || state.resource.is_some() {
180            return Err(ResourceError::Driver(
181                "resource accounting requires an unacquired, unbound allocation".into(),
182            ));
183        }
184        state.resource = Some((accounting, bytes));
185        Ok(())
186    }
187
188    /// Publish acquisition after the real pointer has an armed storage owner.
189    /// Even an accounting error means storage exists: it must be retained for
190    /// physical cleanup, never reported as a definite malloc refusal.
191    pub fn acquired(&self) -> crate::device_runtime::ResourceResult<()> {
192        self.acquired.store(true, Ordering::Release);
193        let mut state = self.state.lock().map_err(|_| {
194            ResourceError::Driver("allocation accounting poisoned during acquisition".into())
195        })?;
196        if self.was_released() {
197            return Err(ResourceError::Driver(
198                "cannot acquire released storage".into(),
199            ));
200        }
201        if !state.resource_published {
202            if let Some((accounting, bytes)) = &state.resource {
203                accounting.acquire(*bytes)?;
204                state.resource_published = true;
205            }
206        }
207        if !state.local_published {
208            if let Some(charge) = &state.charge {
209                let _mutation = charge.accounting.mutation_lock.lock().map_err(|_| {
210                    ResourceError::Driver("local allocation accounting poisoned".into())
211                })?;
212                let previous = charge.accounting.allocated.load(Ordering::SeqCst);
213                let admitted = previous.checked_add(charge.bytes).ok_or_else(|| {
214                    ResourceError::Driver("local admitted allocation accounting overflow".into())
215                })?;
216                charge
217                    .accounting
218                    .allocated
219                    .store(admitted, Ordering::SeqCst);
220                charge.accounting.peak.fetch_max(admitted, Ordering::SeqCst);
221            }
222            state.local_published = true;
223        }
224        Ok(())
225    }
226
227    /// Remains true after release, so an outer error/unwind cannot refund twice.
228    pub fn was_acquired(&self) -> bool {
229        self.acquired.load(Ordering::Acquire)
230    }
231
232    /// Transfer the physical-byte tally before logical detach. The exact raw
233    /// owner settles it even if backend destruction or unwind bypasses its queue.
234    pub(crate) fn attach_pending(
235        &self,
236        accounting: Arc<std::sync::atomic::AtomicUsize>,
237        bytes: usize,
238    ) -> crate::device_runtime::ResourceResult<()> {
239        let mut state = self.state.lock().map_err(|_| {
240            ResourceError::Driver("allocation accounting poisoned during detach".into())
241        })?;
242        if !self.was_acquired() || self.was_released() || state.pending.is_some() {
243            return Err(ResourceError::Driver(
244                "pending charge requires acquired live storage".into(),
245            ));
246        }
247        accounting
248            .try_update(Ordering::SeqCst, Ordering::SeqCst, |value| {
249                value.checked_add(bytes)
250            })
251            .map_err(|_| ResourceError::Driver("pending allocation accounting overflow".into()))?;
252        state.pending = Some((accounting, bytes));
253        Ok(())
254    }
255
256    /// Called only after the raw owner proves physical free. Accounting failure
257    /// retains the exact charge for retry; no CUDA wait runs under this mutex.
258    ///
259    /// # Safety
260    /// The exact allocation must have been physically freed after every device
261    /// use completed. Keep its owner and this ticket until settlement succeeds.
262    /// Publishing this proof for live storage can invalidate access exclusion.
263    pub unsafe fn confirm_physical_release(&self) -> crate::device_runtime::ResourceResult<()> {
264        self.complete()
265    }
266
267    // Internal storage owners already establish the physical-release boundary.
268    // Custom resource implementations cross the unsafe public boundary above.
269    pub(crate) fn complete(&self) -> crate::device_runtime::ResourceResult<()> {
270        if self.was_acquired() && !self.was_released() {
271            // Acquisition publication itself may have failed after malloc.
272            // Complete its unsettled bookkeeping before recording the release.
273            if let Err(error) = self.acquired() {
274                // A concurrent completion may have settled while this caller
275                // was acquiring the state lock. Its proof is authoritative.
276                if self.was_released() {
277                    return Ok(());
278                }
279                return Err(error);
280            }
281        }
282        let (charge, resource, pending) = {
283            let mut state = self.state.lock().map_err(|_| {
284                ResourceError::Driver("allocation reclamation state poisoned during release".into())
285            })?;
286            if self.was_released() {
287                return Ok(());
288            }
289            if let Some(charge) = &state.charge {
290                if !state.local_published {
291                    return Err(ResourceError::Driver(
292                        "local allocation acquisition was not published".into(),
293                    ));
294                }
295                charge
296                    .accounting
297                    .release_owned_allocation(charge.bytes)
298                    .map_err(|error| ResourceError::Driver(error.to_string()))?;
299            }
300            // Remove each settled obligation before the next fallible step.
301            // A later retry cannot refund an already settled local charge.
302            let charge = state.charge.take();
303            if let Some((accounting, bytes)) = &state.resource {
304                if !state.resource_published {
305                    return Err(ResourceError::Driver(
306                        "resource allocation acquisition was not published".into(),
307                    ));
308                }
309                accounting.release(*bytes)?;
310            }
311            let resource = state.resource.take();
312            if let Some((accounting, bytes)) = &state.pending {
313                accounting
314                    .try_update(Ordering::SeqCst, Ordering::SeqCst, |value| {
315                        value.checked_sub(*bytes)
316                    })
317                    .map_err(|_| {
318                        ResourceError::Driver("pending allocation accounting underflow".into())
319                    })?;
320            }
321            self.released.store(true, Ordering::Release);
322            (charge, resource, state.pending.take())
323        };
324        drop((charge, resource, pending));
325        Ok(())
326    }
327
328    pub(crate) fn was_released(&self) -> bool {
329        self.released.load(Ordering::Acquire)
330    }
331
332    pub(crate) fn release_proof(&self) -> Arc<std::sync::atomic::AtomicBool> {
333        Arc::clone(&self.released)
334    }
335}
336
337/// Until malloc succeeds, a local claim belongs to this calling frame. A token
338/// refusal restores unused token bytes; an ordinary refusal returns its budget.
339/// After acquisition the raw owner, not this guard, settles the claim.
340struct LocalAllocationAttempt<'a> {
341    accounting: Arc<GpuMemoryAccounting>,
342    bytes: u64,
343    remaining: Option<&'a mut u64>,
344    reclamation: Arc<AllocationReclamation>,
345}
346
347impl<'a> LocalAllocationAttempt<'a> {
348    fn new(
349        accounting: Arc<GpuMemoryAccounting>,
350        bytes: u64,
351        remaining: Option<&'a mut u64>,
352    ) -> Self {
353        let reclamation = Arc::new(AllocationReclamation::default());
354        let attempt = Self {
355            accounting,
356            bytes,
357            remaining,
358            reclamation,
359        };
360        attempt
361            .reclamation
362            .attach_local(Arc::clone(&attempt.accounting), bytes)
363            .expect("new allocation attempt has no bound charge");
364        attempt
365    }
366}
367
368impl Drop for LocalAllocationAttempt<'_> {
369    fn drop(&mut self) {
370        if self.reclamation.was_acquired() {
371            return;
372        }
373        if let Some(remaining) = self.remaining.as_mut() {
374            **remaining = remaining
375                .checked_add(self.bytes)
376                .expect("unused allocation reservation remains representable");
377        } else if let Err(error) = self.accounting.release_reserved(self.bytes) {
378            eprintln!("local allocation reservation rollback failed: {error}");
379        }
380    }
381}
382
383/// The backend remains the physical owner while a returned block is being
384/// bound to a typed wrapper. Preserve logical-detach work through any error or
385/// unwind, using the same cold queue as final storage retirement.
386pub(crate) struct ResourceBlockRetirement {
387    payload: Option<ResourceBlockRetirementPayload>,
388}
389
390struct ResourceBlockRetirementPayload {
391    block: DeviceBlock,
392    resource: Arc<dyn crate::device_runtime::DeviceMemoryResource + Send + Sync>,
393    reclamation: Arc<AllocationReclamation>,
394    detached: bool,
395}
396
397impl ResourceBlockRetirement {
398    pub(crate) fn new(
399        block: DeviceBlock,
400        resource: Arc<dyn crate::device_runtime::DeviceMemoryResource + Send + Sync>,
401        reclamation: Arc<AllocationReclamation>,
402    ) -> Self {
403        Self {
404            payload: Some(ResourceBlockRetirementPayload {
405                block,
406                resource,
407                reclamation,
408                detached: false,
409            }),
410        }
411    }
412
413    pub(crate) fn block(&self) -> &DeviceBlock {
414        &self
415            .payload
416            .as_ref()
417            .expect("unpublished block present")
418            .block
419    }
420
421    pub(crate) fn into_block(mut self) -> DeviceBlock {
422        self.payload.take().expect("published block present").block
423    }
424
425    fn release(&mut self) -> crate::device_runtime::ResourceResult<()> {
426        reclaim_allocation(&mut self.payload, |owner| {
427            if owner.reclamation.was_released() {
428                return Ok(());
429            }
430            let mut detach_error = None;
431            let mut detach_panic = None;
432            if !owner.detached {
433                let block = &owner.block;
434                // This is a retry of exact generation-checked logical detach,
435                // not a second physical free. The backend owns that transition.
436                let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
437                    owner.resource.deallocate(DeviceBlock {
438                        ptr: block.ptr,
439                        device_ordinal: block.device_ordinal,
440                        alloc_stream: block.alloc_stream,
441                        bytes: block.bytes,
442                        align: block.align,
443                        tag: block.tag,
444                        generation: block.generation,
445                        state: block.state,
446                    })
447                }));
448                match result {
449                    Ok(Ok(())) | Ok(Err(ResourceError::UseAfterFree { .. })) => {
450                        owner.detached = true
451                    }
452                    Ok(Err(error)) => detach_error = Some(error),
453                    Err(panic) => detach_panic = Some(panic),
454                }
455            }
456            // A decorator can unwind after the backend accepted logical
457            // detach. Still offer that exact backend its physical reap; retrying
458            // only the decorator would strand an already-pending allocation.
459            let reaped = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
460                owner.resource.reap_pending()
461            }));
462            if let Some(panic) = detach_panic {
463                // Both operations have been attempted. Preserve the first
464                // failure even when a telemetry sink also unwinds during reap.
465                drop(reaped);
466                std::panic::resume_unwind(panic);
467            }
468            let reaped = match reaped {
469                Ok(result) => result,
470                Err(panic) => std::panic::resume_unwind(panic),
471            };
472            allocation_reclamation_outcome(
473                owner.reclamation.was_released(),
474                detach_error.map_or(reaped, Err),
475            )
476        })
477    }
478}
479
480impl Drop for ResourceBlockRetirement {
481    fn drop(&mut self) {
482        if let Some(payload) = self.payload.take() {
483            let mut owner = Self {
484                payload: Some(payload),
485            };
486            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
487                crate::cuda_graph::retry_retirement_after_stream_captures(move || {
488                    owner.release().is_ok()
489                });
490            }));
491        }
492    }
493}
494
495/// One atomic claim on a [`GpuMemoryManager`] budget.
496///
497/// The claim protects a complete multi-allocation request from competing
498/// callers. Bytes remain reserved until they are transferred to returned
499/// allocation owners or released when this token is dropped.
500#[must_use = "dropping the reservation immediately releases its unused budget"]
501pub struct GpuMemoryReservation {
502    manager: Arc<GpuMemoryManager>,
503    runtime_reservation: Option<RuntimeMemoryReservation>,
504    total_bytes: u64,
505    remaining_bytes: u64,
506}
507
508impl std::fmt::Debug for GpuMemoryReservation {
509    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510        f.debug_struct("GpuMemoryReservation")
511            .field("total_bytes", &self.total_bytes)
512            .field("remaining_bytes", &self.remaining_bytes)
513            .finish()
514    }
515}
516
517impl GpuMemoryReservation {
518    /// Stable address of the memory manager that admitted this reservation.
519    pub fn memory_manager_ptr_value(&self) -> usize {
520        Arc::as_ptr(&self.manager) as usize
521    }
522
523    /// Complete byte claim made when this reservation was created.
524    pub fn total_bytes(&self) -> u64 {
525        self.total_bytes
526    }
527
528    /// Bytes still available for materialization through this token.
529    pub fn remaining_bytes(&self) -> u64 {
530        self.remaining_bytes
531    }
532
533    /// Bytes already transferred to allocation owners.
534    pub fn used_bytes(&self) -> u64 {
535        self.total_bytes - self.remaining_bytes
536    }
537
538    /// Allocate typed device memory from this reservation.
539    pub fn alloc<T: cudarc::driver::DeviceRepr>(
540        &mut self,
541        len: usize,
542    ) -> Result<TrackedCudaSlice<T>> {
543        self.manager
544            .accounting
545            .alloc_count
546            .fetch_add(1, Ordering::Relaxed);
547        let bytes = (len as u64)
548            .checked_mul(std::mem::size_of::<T>() as u64)
549            .ok_or_else(|| XlogError::Kernel("Allocation size overflow".to_string()))?;
550        let used = self.used_bytes();
551        if bytes > self.remaining_bytes {
552            return Err(MemoryPressure {
553                layer: "manager_reservation_alloc",
554                current_bytes: used as u128,
555                requested_bytes: bytes as u128,
556                budget_bytes: self.total_bytes,
557                prior_peak_bytes: self.manager.accounting.peak.load(Ordering::SeqCst),
558            }
559            .into_error());
560        }
561
562        self.remaining_bytes -= bytes;
563        let manager = Arc::clone(&self.manager);
564        let attempt = LocalAllocationAttempt::new(
565            Arc::clone(&manager.accounting),
566            bytes,
567            Some(&mut self.remaining_bytes),
568        );
569        let runtime_reservation = self.runtime_reservation.as_mut();
570        manager
571            .alloc_after_local_reservation::<T>(
572                len,
573                bytes,
574                runtime_reservation,
575                Arc::clone(&attempt.reclamation),
576            )
577            .map_err(|error| map_resource_error(error, manager.peak_bytes()))
578    }
579
580    /// Allocate raw device bytes from this reservation through the attached
581    /// runtime resource stack.
582    pub fn alloc_raw(&mut self, bytes: usize, tag: AllocTag) -> Result<RuntimeAllocBlock> {
583        let bytes_u64 = u64::try_from(bytes)
584            .map_err(|_| XlogError::Kernel("Allocation size overflow".to_string()))?;
585        let used = self.used_bytes();
586        if bytes_u64 > self.remaining_bytes {
587            return Err(MemoryPressure {
588                layer: "manager_reservation_alloc_raw",
589                current_bytes: used as u128,
590                requested_bytes: bytes_u64 as u128,
591                budget_bytes: self.total_bytes,
592                prior_peak_bytes: self.manager.accounting.peak.load(Ordering::SeqCst),
593            }
594            .into_error());
595        }
596
597        self.remaining_bytes -= bytes_u64;
598        let manager = Arc::clone(&self.manager);
599        let attempt = LocalAllocationAttempt::new(
600            Arc::clone(&manager.accounting),
601            bytes_u64,
602            Some(&mut self.remaining_bytes),
603        );
604        let runtime_reservation = self.runtime_reservation.as_mut();
605        manager
606            .alloc_raw_after_local_reservation(
607                bytes,
608                bytes_u64,
609                tag,
610                runtime_reservation,
611                Arc::clone(&attempt.reclamation),
612            )
613            .map_err(|error| map_resource_error(error, manager.peak_bytes()))
614    }
615}
616
617impl Drop for GpuMemoryReservation {
618    fn drop(&mut self) {
619        let unused = std::mem::take(&mut self.remaining_bytes);
620        let release = self.manager.rollback_local_reservation(unused);
621        debug_assert!(release.is_ok(), "reservation release must be balanced");
622    }
623}
624
625struct MemoryPressure {
626    layer: &'static str,
627    current_bytes: u128,
628    requested_bytes: u128,
629    budget_bytes: u64,
630    prior_peak_bytes: u64,
631}
632
633impl MemoryPressure {
634    fn required_bytes(&self) -> u128 {
635        self.current_bytes + self.requested_bytes
636    }
637
638    fn into_error(self) -> XlogError {
639        let required_bytes = self.required_bytes();
640        let required_u64_overflow = required_bytes > u64::MAX as u128;
641        XlogError::ResourceExhausted {
642            context: format!(
643                "GPU memory pressure: layer={} current_bytes={} requested_bytes={} required_bytes={} required_u64_overflow={} budget_bytes={} prior_peak_bytes={}",
644                self.layer,
645                self.current_bytes,
646                self.requested_bytes,
647                required_bytes,
648                required_u64_overflow,
649                self.budget_bytes,
650                self.prior_peak_bytes,
651            ),
652            estimated_bytes: u64::try_from(required_bytes).unwrap_or(u64::MAX),
653            budget_bytes: self.budget_bytes,
654        }
655    }
656}
657
658/// The actual allocation owner, shared by every typed view and retained use.
659struct DeviceStorage {
660    backing: ManuallyDrop<Backing>,
661    stream: Arc<CudaStream>,
662    dependencies: Arc<DeviceAccessDependencies>,
663}
664
665enum Backing {
666    Native(Arc<RawDeviceAllocation>),
667    Runtime(RuntimeAllocBlock),
668    Foreign {
669        _owner: Box<dyn Send + Sync>,
670        _source: Option<Arc<TrackedCudaSlice<u8>>>,
671        bytes: u64,
672    },
673}
674
675/// One native allocation owner shared by the typed and runtime allocators.
676/// No cudarc CudaSlice is constructed: its post-malloc event construction and
677/// implicit Drop free cannot express retained initialization/release failures.
678pub(crate) struct RawDeviceAllocation {
679    payload: Option<RawAllocationPayload>,
680    reclamation_admission: Option<MemoryUseGroup>,
681}
682
683struct RawAllocationPayload {
684    ptr: u64,
685    acquired: bool,
686    bytes: usize,
687    stream: Arc<CudaStream>,
688    allocation_stream: Arc<CudaStream>,
689    dependencies: Option<Arc<DeviceAccessDependencies>>,
690    reclamation: Arc<AllocationReclamation>,
691    asynchronous: bool,
692    free_state: DriverReleaseState,
693    manager: Option<Arc<GpuMemoryManager>>,
694    lifecycle_exclusion: Option<AllocationLifecyclePermit>,
695}
696
697/// Serialize only physical allocation/release address reuse within one CUDA
698/// context. Operation admission and device-resident execution never enter it.
699struct AllocationLifecycleExclusion {
700    active: std::sync::Mutex<bool>,
701    changed: std::sync::Condvar,
702}
703
704struct AllocationLifecyclePermit {
705    exclusion: Arc<AllocationLifecycleExclusion>,
706}
707
708impl AllocationLifecycleExclusion {
709    fn enter(self: &Arc<Self>) -> crate::device_runtime::ResourceResult<AllocationLifecyclePermit> {
710        let mut active = self.active.lock().map_err(|_| {
711            ResourceError::Driver("CUDA allocation lifecycle exclusion poisoned".into())
712        })?;
713        while *active {
714            active = self.changed.wait(active).map_err(|_| {
715                ResourceError::Driver("CUDA allocation lifecycle exclusion poisoned".into())
716            })?;
717        }
718        *active = true;
719        Ok(AllocationLifecyclePermit {
720            exclusion: Arc::clone(self),
721        })
722    }
723}
724
725impl Drop for AllocationLifecyclePermit {
726    fn drop(&mut self) {
727        let mut active = self
728            .exclusion
729            .active
730            .lock()
731            .unwrap_or_else(|error| error.into_inner());
732        *active = false;
733        self.exclusion.changed.notify_one();
734    }
735}
736
737fn allocation_lifecycle_exclusion(
738    context: usize,
739) -> crate::device_runtime::ResourceResult<Arc<AllocationLifecycleExclusion>> {
740    static EXCLUSIONS: std::sync::OnceLock<
741        std::sync::Mutex<
742            std::collections::HashMap<usize, std::sync::Weak<AllocationLifecycleExclusion>>,
743        >,
744    > = std::sync::OnceLock::new();
745    let mut exclusions = EXCLUSIONS
746        .get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
747        .lock()
748        .map_err(|_| ResourceError::Driver("CUDA allocation lifecycle registry poisoned".into()))?;
749    if let Some(exclusion) = exclusions.get(&context).and_then(std::sync::Weak::upgrade) {
750        return Ok(exclusion);
751    }
752    let exclusion = Arc::new(AllocationLifecycleExclusion {
753        active: std::sync::Mutex::new(false),
754        changed: std::sync::Condvar::new(),
755    });
756    exclusions.insert(context, Arc::downgrade(&exclusion));
757    Ok(exclusion)
758}
759
760#[derive(Default, PartialEq, Eq)]
761enum DriverReleaseState {
762    #[default]
763    Owned,
764    OutcomeUnknown,
765    Submitted,
766}
767
768impl DriverReleaseState {
769    #[cfg(test)]
770    fn confirm_async(
771        &self,
772        recorded: &mut bool,
773        record: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
774        wait: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
775    ) -> crate::device_runtime::ResourceResult<()> {
776        if *self != Self::Submitted {
777            return Err(ResourceError::Driver(
778                "completion cannot prove an unconfirmed allocation free submission".into(),
779            ));
780        }
781        if !*recorded {
782            record()?;
783            *recorded = true;
784        }
785        wait()
786    }
787
788    fn submit(
789        &mut self,
790        free: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
791    ) -> crate::device_runtime::ResourceResult<()> {
792        match self {
793            Self::Submitted => return Ok(()),
794            Self::OutcomeUnknown => {
795                return Err(ResourceError::Driver(
796                    "previous resource release outcome is unknown; refusing a second release"
797                        .into(),
798                ))
799            }
800            Self::Owned => {}
801        }
802        *self = Self::OutcomeUnknown;
803        free()?;
804        *self = Self::Submitted;
805        Ok(())
806    }
807}
808
809impl RawDeviceAllocation {
810    pub(crate) fn allocate(
811        stream: Arc<CudaStream>,
812        allocation_stream: Arc<CudaStream>,
813        bytes: usize,
814        manager: Option<Arc<GpuMemoryManager>>,
815        reclamation: Arc<AllocationReclamation>,
816    ) -> crate::device_runtime::ResourceResult<Arc<Self>> {
817        let _capture_exclusion = crate::cuda_graph::reserve_uncaptured_stream(&stream)?;
818        let asynchronous = stream.context().has_async_alloc();
819        stream.context().bind_to_thread()?;
820        // Preserve the caller-prefix ordering previously carried by a private
821        // ready event without retaining an event per allocation.
822        stream.synchronize()?;
823        // Arm both stream owners before malloc. A post-malloc failure therefore
824        // reaches the canonical cold reaper with the exact device-owned stream.
825        let mut allocation = initialize_allocation(
826            Self {
827                reclamation_admission: None,
828                payload: Some(RawAllocationPayload {
829                    ptr: 0,
830                    acquired: false,
831                    bytes,
832                    stream,
833                    allocation_stream,
834                    dependencies: None,
835                    reclamation: Arc::clone(&reclamation),
836                    asynchronous,
837                    free_state: DriverReleaseState::Owned,
838                    manager,
839                    lifecycle_exclusion: None,
840                }),
841            },
842            bytes,
843            Arc::clone(&reclamation),
844            |allocation| {
845                let payload = allocation
846                    .payload
847                    .as_mut()
848                    .expect("allocation owner present");
849                // Keep the permit in the armed owner so a fallible
850                // post-malloc step transfers it to the cold reaper.
851                payload.lifecycle_exclusion = Some(
852                    allocation_lifecycle_exclusion(payload.stream.context().cu_ctx() as usize)?
853                        .enter()?,
854                );
855                let ptr = if bytes == 0 {
856                    0
857                } else {
858                    // SAFETY: capture is excluded and the device-owned stream
859                    // is serialized by the allocation lifecycle permit.
860                    unsafe {
861                        if asynchronous {
862                            cudarc::driver::result::malloc_async(
863                                payload.allocation_stream.cu_stream(),
864                                bytes,
865                            )?
866                        } else {
867                            cudarc::driver::result::malloc_sync(bytes)?
868                        }
869                    }
870                };
871                payload.ptr = ptr;
872                payload.acquired = true;
873                payload.reclamation.acquired()?;
874                let poison = payload.manager.is_some() && bytes != 0 && poison_alloc_enabled();
875                if payload.manager.is_some() {
876                    alloc_guard_insert(payload.ptr, bytes as u64);
877                    if poison {
878                        // SAFETY: initialized only by this armed allocation owner.
879                        unsafe {
880                            cudarc::driver::sys::cuMemsetD8Async(
881                                payload.ptr,
882                                0xDD,
883                                bytes,
884                                payload.allocation_stream.cu_stream(),
885                            )
886                            .result()?;
887                        }
888                    }
889                }
890                if (asynchronous && bytes != 0) || poison {
891                    // SAFETY: the lifecycle permit excludes any other user of
892                    // this device-owned allocation stream until publication.
893                    unsafe {
894                        cudarc::driver::result::stream::synchronize(
895                            payload.allocation_stream.cu_stream(),
896                        )?;
897                    }
898                }
899                payload.dependencies = Some(Arc::new(DeviceAccessDependencies::after_ready(
900                    Arc::clone(payload.stream.context()),
901                    Arc::clone(&reclamation),
902                )));
903                Ok(())
904            },
905        )?;
906        // Take the permit while the plain owner is still uniquely mutable.
907        let pending_lifecycle_exclusion = allocation
908            .payload
909            .as_mut()
910            .expect("initialized allocation")
911            .lifecycle_exclusion
912            .take()
913            .expect("allocation lifecycle exclusion held");
914        let allocation = Arc::new(allocation);
915        // Declare the moved permit after the Arc so error or unwind releases
916        // the exclusion before Arc::drop synchronously enters the cold reaper.
917        let lifecycle_exclusion = pending_lifecycle_exclusion;
918        let dependencies = allocation.dependencies();
919        dependencies.bind_allocation(&allocation);
920        {
921            let owner: Arc<dyn MemoryStorageOwner> =
922                Arc::clone(&allocation) as Arc<dyn MemoryStorageOwner>;
923            let payload = allocation.payload.as_ref().expect("initialized allocation");
924            let range = match MemoryUse::new(payload.ptr, payload.bytes, Access::ReadWrite) {
925                Ok(range) => range,
926                Err(error) => return Err(error.retaining(bytes, reclamation)),
927            };
928            block_use_registry()
929                .lock()
930                .expect("device block-use registry poisoned")
931                .register_storage(
932                    payload.stream.context().cu_ctx() as usize,
933                    range,
934                    Arc::downgrade(&owner),
935                    dependencies.reclamation.release_proof(),
936                );
937        }
938        drop(lifecycle_exclusion);
939        Ok(allocation)
940    }
941
942    pub(crate) fn ptr(&self) -> u64 {
943        self.payload.as_ref().expect("allocation live").ptr
944    }
945    pub(crate) fn len(&self) -> usize {
946        self.payload.as_ref().expect("allocation live").bytes
947    }
948    pub(crate) fn dependencies(&self) -> Arc<DeviceAccessDependencies> {
949        Arc::clone(
950            self.payload
951                .as_ref()
952                .expect("allocation live")
953                .dependencies
954                .as_ref()
955                .expect("allocation producer was recorded"),
956        )
957    }
958
959    pub(crate) fn reclamation(&self) -> &Arc<AllocationReclamation> {
960        &self
961            .payload
962            .as_ref()
963            .expect("allocation owner remains live")
964            .reclamation
965    }
966
967    /// Cold physical reclamation. A successful async free is never submitted a
968    /// second time, even if its following completion check fails.
969    pub(crate) fn release(&mut self) -> crate::device_runtime::ResourceResult<()> {
970        let admission = &mut self.reclamation_admission;
971        reclaim_allocation(&mut self.payload, |payload| {
972            // No memory was acquired; the device owns the shared allocation
973            // stream and the calling frame restores its provisional budget.
974            if !payload.acquired {
975                return Ok(());
976            }
977            if payload.lifecycle_exclusion.is_none() {
978                // A failed or unknown release keeps this permit in the
979                // retryable payload until physical release is proven.
980                payload.lifecycle_exclusion = Some(
981                    allocation_lifecycle_exclusion(payload.stream.context().cu_ctx() as usize)?
982                        .enter()?,
983                );
984            }
985            crate::device_runtime::resource::with_reclamation_admission(
986                block_use_registry(),
987                payload.stream.context().cu_ctx() as usize,
988                MemoryUse::new(payload.ptr, payload.bytes, Access::ReadWrite)?,
989                payload.reclamation.release_proof(),
990                admission,
991                || {
992                    // The device-owned stream is serialized by the lifecycle
993                    // permit and is never exposed as an execution dependency.
994                    let _capture_exclusion = crate::cuda_graph::reserve_capture_exclusion()?;
995                    payload.stream.context().bind_to_thread()?;
996                    if payload.free_state != DriverReleaseState::Submitted {
997                        if payload.free_state == DriverReleaseState::OutcomeUnknown {
998                            return Err(ResourceError::Driver(
999                                "previous allocation free outcome is unknown".into(),
1000                            ));
1001                        }
1002                        if let Some(dependencies) = &payload.dependencies {
1003                            dependencies.synchronize()?;
1004                        }
1005                        payload.stream.context().bind_to_thread()?;
1006                        let poison = payload.manager.is_some()
1007                            && payload.bytes != 0
1008                            && poison_free_enabled();
1009                        if poison {
1010                            // SAFETY: all users completed and no free was attempted.
1011                            // A failed poison wait may retry on the same serialized
1012                            // device-owned stream, where earlier poison is ordered.
1013                            unsafe {
1014                                cudarc::driver::sys::cuMemsetD8Async(
1015                                    payload.ptr,
1016                                    0xDD,
1017                                    payload.bytes,
1018                                    payload.allocation_stream.cu_stream(),
1019                                )
1020                                .result()?;
1021                            }
1022                            // SAFETY: the lifecycle permit owns the stream prefix.
1023                            unsafe {
1024                                cudarc::driver::result::stream::synchronize(
1025                                    payload.allocation_stream.cu_stream(),
1026                                )?;
1027                            }
1028                        }
1029                        payload.free_state.submit(|| {
1030                            if payload.bytes != 0 {
1031                                // Stop advertising a logically live range before the driver
1032                                // can reuse its address for another allocation. The armed
1033                                // owner still retains storage/budget on an unknown outcome.
1034                                if payload.manager.is_some() {
1035                                    alloc_guard_remove(payload.ptr);
1036                                }
1037                                // SAFETY: all actual-use fences completed; free matches the
1038                                // chosen allocation mode and the exact allocation context.
1039                                unsafe {
1040                                    if payload.asynchronous {
1041                                        cudarc::driver::result::free_async(
1042                                            payload.ptr,
1043                                            payload.allocation_stream.cu_stream(),
1044                                        )?;
1045                                    } else {
1046                                        cudarc::driver::result::free_sync(payload.ptr)?;
1047                                    }
1048                                }
1049                            }
1050                            Ok(())
1051                        })?;
1052                    }
1053                    if payload.asynchronous && payload.bytes != 0 {
1054                        // A failed synchronization retains Submitted state, so
1055                        // retry proves the same free prefix without resubmission.
1056                        unsafe {
1057                            cudarc::driver::result::stream::synchronize(
1058                                payload.allocation_stream.cu_stream(),
1059                            )?;
1060                        }
1061                    }
1062                    payload.reclamation.complete()?;
1063                    Ok(())
1064                },
1065            )
1066        })
1067    }
1068}
1069
1070impl MemoryStorageOwner for RawDeviceAllocation {
1071    fn dependencies(&self) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
1072        Ok(RawDeviceAllocation::dependencies(self))
1073    }
1074}
1075
1076/// Keep the actual payload across a failed or unwinding reclamation attempt.
1077/// Only a completed physical release and its accounting may retire the owner.
1078pub(crate) fn reclaim_allocation<T>(
1079    payload: &mut Option<T>,
1080    release: impl FnOnce(&mut T) -> crate::device_runtime::ResourceResult<()>,
1081) -> crate::device_runtime::ResourceResult<()> {
1082    if let Some(owner) = payload.as_mut() {
1083        release(owner)?;
1084        *payload = None;
1085    }
1086    Ok(())
1087}
1088
1089/// Reclaim only the unique physical owner. A shared owner is returned unchanged
1090/// to the existing pending queue. After uniqueness wins, the old Weak handles
1091/// can no longer mint a new lease; a failed attempt restores the real payload
1092/// for the next reaper, including when the driver boundary unwinds.
1093pub(crate) fn reclaim_shared_allocation<T>(
1094    pending: &mut Option<Arc<T>>,
1095    release: impl FnOnce(&mut T) -> crate::device_runtime::ResourceResult<()>,
1096) -> crate::device_runtime::ResourceResult<bool> {
1097    let Some(shared) = pending.take() else {
1098        return Ok(true);
1099    };
1100    let owner = match Arc::try_unwrap(shared) {
1101        Ok(owner) => owner,
1102        Err(shared) => {
1103            *pending = Some(shared);
1104            return Ok(false);
1105        }
1106    };
1107    struct RestoreOwner<'a, T> {
1108        pending: &'a mut Option<Arc<T>>,
1109        owner: Option<T>,
1110    }
1111    impl<T> Drop for RestoreOwner<'_, T> {
1112        fn drop(&mut self) {
1113            if let Some(owner) = self.owner.take() {
1114                *self.pending = Some(Arc::new(owner));
1115            }
1116        }
1117    }
1118    let mut guard = RestoreOwner {
1119        pending,
1120        owner: Some(owner),
1121    };
1122    release(guard.owner.as_mut().expect("physical owner present"))?;
1123    drop(guard.owner.take());
1124    Ok(true)
1125}
1126
1127/// Restore every unfinished actual owner to its existing pending queue on
1128/// return or unwind. The merge moves owners and empties the drained container.
1129pub(crate) struct PendingReclamationBatch<'a, T> {
1130    queue: &'a std::sync::Mutex<T>,
1131    owners: ManuallyDrop<T>,
1132    merge: fn(&mut T, &mut T),
1133}
1134
1135impl<'a, T: Default> PendingReclamationBatch<'a, T> {
1136    pub(crate) fn take(queue: &'a std::sync::Mutex<T>, merge: fn(&mut T, &mut T)) -> Self {
1137        let owners = {
1138            let mut pending = queue.lock().expect("pending allocation queue poisoned");
1139            ManuallyDrop::new(std::mem::take(&mut *pending))
1140        };
1141        Self {
1142            queue,
1143            owners,
1144            merge,
1145        }
1146    }
1147}
1148
1149impl<T> std::ops::Deref for PendingReclamationBatch<'_, T> {
1150    type Target = T;
1151    fn deref(&self) -> &T {
1152        &self.owners
1153    }
1154}
1155
1156impl<T> std::ops::DerefMut for PendingReclamationBatch<'_, T> {
1157    fn deref_mut(&mut self) -> &mut T {
1158        &mut self.owners
1159    }
1160}
1161
1162impl<T> Drop for PendingReclamationBatch<'_, T> {
1163    fn drop(&mut self) {
1164        // A failed driver wait may unwind. Return its real owner, and every
1165        // unvisited owner, to the same queue before the next reaper runs.
1166        // Merge functions move ownership only; a panic keeps any unmerged
1167        // remainder armed rather than dropping CUDA owners under this mutex.
1168        let mut queue = self.queue.lock().unwrap_or_else(|error| error.into_inner());
1169        (self.merge)(&mut *queue, &mut self.owners);
1170        drop(queue);
1171        // SAFETY: the merge emptied the batch; its container metadata is no
1172        // longer protected by the pending queue mutex.
1173        unsafe { ManuallyDrop::drop(&mut self.owners) };
1174    }
1175}
1176
1177/// Both allocator backends reap the same actual owners. A leased entry is
1178/// skipped without reserving a free; failures leave every unfinished slot
1179/// owned by the caller's existing pending batch.
1180pub(crate) fn reap_raw_allocations(
1181    allocations: &mut Vec<Option<Arc<RawDeviceAllocation>>>,
1182) -> crate::device_runtime::ResourceResult<()> {
1183    for allocation in allocations.iter_mut() {
1184        reclaim_shared_allocation(allocation, RawDeviceAllocation::release)?;
1185    }
1186    allocations.retain(Option::is_some);
1187    Ok(())
1188}
1189
1190impl Drop for RawDeviceAllocation {
1191    fn drop(&mut self) {
1192        if let Some(payload) = self.payload.take() {
1193            let mut owner = Self {
1194                payload: Some(payload),
1195                reclamation_admission: self.reclamation_admission.take(),
1196            };
1197            // The canonical cold queue retains this callable physical owner
1198            // across errors and unwind. Even a final Drop must not turn a
1199            // retryable fence failure into an unreachable ManuallyDrop leak.
1200            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1201                crate::cuda_graph::retry_retirement_after_stream_captures(move || {
1202                    if let Err(error) = owner.release() {
1203                        if let Some(manager) = owner
1204                            .payload
1205                            .as_ref()
1206                            .filter(|p| !p.reclamation.was_released())
1207                            .and_then(|p| p.manager.as_ref())
1208                        {
1209                            manager.record_deallocation_failure(owner.len() as u64);
1210                        }
1211                        eprintln!(
1212                            "CUDA allocation cleanup incomplete; retaining unresolved memory or handle ownership in the cold reaper: {error}"
1213                        );
1214                        return false;
1215                    }
1216                    true
1217                });
1218            }));
1219        }
1220    }
1221}
1222
1223/// Arm the real allocation before any fallible post-allocation initialization.
1224/// Success transfers it to the ordinary storage owner. On error or unwind its
1225/// dropping owner reaches cold retirement; the error shares only its ticket.
1226fn initialize_allocation<T: Send + Sync + 'static>(
1227    mut owner: T,
1228    bytes: usize,
1229    reclamation: Arc<AllocationReclamation>,
1230    initialize: impl FnOnce(&mut T) -> crate::device_runtime::ResourceResult<()>,
1231) -> crate::device_runtime::ResourceResult<T> {
1232    match initialize(&mut owner) {
1233        Ok(()) => Ok(owner),
1234        Err(error) if reclamation.was_acquired() => Err(error.retaining(bytes, reclamation)),
1235        Err(error) => Err(error),
1236    }
1237}
1238
1239// DeviceStorage never exposes the opaque payload through a shared reference.
1240// All mutable bookkeeping is locked; its retirement payload is armed against
1241// unwind before release. This is also the Arrow custom-allocation contract.
1242impl std::panic::RefUnwindSafe for DeviceStorage {}
1243
1244impl DeviceStorage {
1245    fn new(backing: Backing, stream: Arc<CudaStream>, raw_ptr: u64) -> Arc<Self> {
1246        let dependencies = match &backing {
1247            Backing::Native(allocation) => allocation.dependencies(),
1248            Backing::Runtime(allocation) => Arc::clone(&allocation.dependencies),
1249            Backing::Foreign { .. } => Arc::new(DeviceAccessDependencies::after_ready(
1250                Arc::clone(stream.context()),
1251                Arc::default(),
1252            )),
1253        };
1254        let storage = Arc::new(Self {
1255            backing: ManuallyDrop::new(backing),
1256            stream,
1257            dependencies,
1258        });
1259        // Native and runtime storage already register the actual raw owner.
1260        // A typed wrapper can disappear while its raw lease remains usable;
1261        // registering that wrapper would leave a false retirement tombstone.
1262        // Foreign storage instead owns the producer's actual deleter token.
1263        if matches!(&*storage.backing, Backing::Foreign { .. }) {
1264            let owner: Arc<dyn MemoryStorageOwner> = storage.clone();
1265            let range = MemoryUse::new(raw_ptr, storage.bytes() as usize, Access::ReadWrite)
1266                .expect("CUDA allocation must describe a representable device range");
1267            block_use_registry()
1268                .lock()
1269                .expect("device block-use registry poisoned")
1270                .register_storage(
1271                    storage.stream.context().cu_ctx() as usize,
1272                    range,
1273                    Arc::downgrade(&owner),
1274                    storage.dependencies.reclamation.release_proof(),
1275                );
1276        }
1277        storage
1278    }
1279
1280    fn manager(&self) -> Option<&Arc<GpuMemoryManager>> {
1281        match &*self.backing {
1282            Backing::Native(allocation) => {
1283                allocation.payload.as_ref().and_then(|p| p.manager.as_ref())
1284            }
1285            Backing::Runtime(allocation) => Some(&allocation.manager),
1286            Backing::Foreign { .. } => None,
1287        }
1288    }
1289
1290    fn bytes(&self) -> u64 {
1291        match &*self.backing {
1292            Backing::Native(allocation) => allocation.len() as u64,
1293            Backing::Runtime(allocation) => allocation.bytes,
1294            Backing::Foreign { bytes, .. } => *bytes,
1295        }
1296    }
1297}
1298
1299impl MemoryStorageOwner for DeviceStorage {
1300    fn dependencies(&self) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
1301        Ok(Arc::clone(&self.dependencies))
1302    }
1303}
1304
1305impl Drop for DeviceStorage {
1306    fn drop(&mut self) {
1307        let backing = unsafe { ManuallyDrop::take(&mut self.backing) };
1308        let dependencies = Arc::clone(&self.dependencies);
1309        let stream = Arc::clone(&self.stream);
1310        match backing {
1311            Backing::Foreign { .. } => crate::cuda_graph::retire_resources_after_completion(
1312                (Some(backing), dependencies, stream),
1313                |(_, dependencies, _)| dependencies.synchronize(),
1314                |owners| {
1315                    // The producer deleter must return before publishing release.
1316                    // Its context and dependencies remain armed if it unwinds;
1317                    // neither the callback nor its deleter may run a second time.
1318                    drop(owners.0.take());
1319                    Ok(())
1320                },
1321                |owners| owners.1.mark_allocation_released(),
1322                |_, error| eprintln!("CUDA imported storage retirement incomplete: {error}"),
1323            ),
1324            backing => crate::cuda_graph::retire_after_stream_captures(move || {
1325                // Native Drop transfers the actual raw owner to cold retirement;
1326                // runtime Drop transfers it to the existing live/pending backend.
1327                // Neither pending nor error keeps a second runtime-owner cycle.
1328                drop(backing);
1329            }),
1330        }
1331    }
1332}
1333
1334/// Debug probe: poison legacy allocations with 0xDD at drop so any
1335/// live alias of freed memory becomes visually distinct. Gated on
1336/// `XLOG_DEBUG_POISON_FREE=1`, read once per process.
1337fn debug_env_enabled(name: &str) -> bool {
1338    resolve_bool(None, name, false)
1339        .unwrap_or_else(|error| panic!("invalid CUDA memory debug configuration: {error}"))
1340}
1341
1342fn poison_free_enabled() -> bool {
1343    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1344    *ENABLED.get_or_init(|| debug_env_enabled("XLOG_DEBUG_POISON_FREE"))
1345}
1346
1347/// Debug probe: poison fresh legacy allocations with 0xDD so reads of
1348/// unwritten contents surface deterministically. Gated on
1349/// `XLOG_DEBUG_POISON_ALLOC=1`, read once per process.
1350fn poison_alloc_enabled() -> bool {
1351    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1352    *ENABLED.get_or_init(|| debug_env_enabled("XLOG_DEBUG_POISON_ALLOC"))
1353}
1354
1355/// Debug probe: track live legacy allocation ranges and panic if the
1356/// allocator ever hands out a region overlapping one that is still
1357/// live (double-hand-out / use-after-free detector, timing
1358/// independent). Gated on `XLOG_DEBUG_ALLOC_GUARD=1`.
1359fn alloc_guard() -> Option<&'static std::sync::Mutex<std::collections::BTreeMap<u64, u64>>> {
1360    static GUARD: std::sync::OnceLock<
1361        Option<std::sync::Mutex<std::collections::BTreeMap<u64, u64>>>,
1362    > = std::sync::OnceLock::new();
1363    GUARD
1364        .get_or_init(|| {
1365            if debug_env_enabled("XLOG_DEBUG_ALLOC_GUARD") {
1366                Some(std::sync::Mutex::new(std::collections::BTreeMap::new()))
1367            } else {
1368                None
1369            }
1370        })
1371        .as_ref()
1372}
1373
1374fn alloc_guard_insert(ptr: u64, bytes: u64) {
1375    let Some(guard) = alloc_guard() else { return };
1376    if bytes == 0 {
1377        return;
1378    }
1379    let mut live = guard.lock().unwrap();
1380    // Overlap check against the nearest live range at or below ptr and
1381    // the first live range above it.
1382    if let Some((&p, &b)) = live.range(..=ptr).next_back() {
1383        if p + b > ptr {
1384            panic!(
1385                "ALLOC GUARD: new allocation [{:#x}, {:#x}) overlaps live [{:#x}, {:#x})",
1386                ptr,
1387                ptr + bytes,
1388                p,
1389                p + b
1390            );
1391        }
1392    }
1393    if let Some((&p, _)) = live.range(ptr + 1..).next() {
1394        if ptr + bytes > p {
1395            panic!(
1396                "ALLOC GUARD: new allocation [{:#x}, {:#x}) overlaps live starting at {:#x}",
1397                ptr,
1398                ptr + bytes,
1399                p
1400            );
1401        }
1402    }
1403    live.insert(ptr, bytes);
1404}
1405
1406fn alloc_guard_remove(ptr: u64) {
1407    let Some(guard) = alloc_guard() else { return };
1408    guard.lock().unwrap().remove(&ptr);
1409}
1410
1411/// A typed view retaining its actual device allocation and memory budget.
1412///
1413/// Native owning slices cannot be extracted through a safe mutable borrow:
1414///
1415/// ```compile_fail
1416/// use xlog_cuda::memory::TrackedCudaSlice;
1417/// use cudarc::driver::CudaSlice;
1418/// fn native_owner(slice: &mut TrackedCudaSlice<u8>) -> &mut CudaSlice<u8> {
1419///     slice
1420/// }
1421/// ```
1422///
1423/// Device access must be prepared by XLOG; a passive allocation handle cannot
1424/// bypass its access reservation through cudarc's safe pointer API:
1425///
1426/// ```compile_fail
1427/// use xlog_cuda::memory::TrackedCudaSlice;
1428/// use cudarc::driver::DevicePtr;
1429/// fn native_read(slice: &TrackedCudaSlice<u8>) {
1430///     fn accepts_native<T: DevicePtr<u8>>(_: &T) {}
1431///     accepts_native(slice);
1432/// }
1433/// ```
1434pub struct TrackedCudaSlice<T: cudarc::driver::DeviceRepr> {
1435    storage: Arc<DeviceStorage>,
1436    ptr: u64,
1437    len: usize,
1438    element: std::marker::PhantomData<T>,
1439}
1440
1441/// A checked typed device span with a strong allocation owner. Subviews and
1442/// reinterpretations preserve this same owner; none expose cudarc pointer traits.
1443pub struct DeviceMemoryView<T> {
1444    ptr: u64,
1445    len: usize,
1446    storage: Arc<DeviceStorage>,
1447    element: std::marker::PhantomData<T>,
1448}
1449
1450/// Physical provenance of a view of an allocation owned by XLOG.
1451///
1452/// Retains the actual allocation, not an address-derived identity. This is
1453/// metadata only: it grants no device access, content validity, mutation
1454/// exclusion, publication lease, or external tensor version-counter identity.
1455#[derive(Clone)]
1456pub struct DeviceAllocationProvenance {
1457    allocation: Arc<RawDeviceAllocation>,
1458    byte_offset: u64,
1459    view_bytes: u64,
1460}
1461
1462impl DeviceAllocationProvenance {
1463    /// Compare the retained physical owners, independently of view geometry.
1464    pub fn same_allocation(&self, other: &Self) -> bool {
1465        Arc::ptr_eq(&self.allocation, &other.allocation)
1466    }
1467
1468    /// Complete byte extent of the actual allocation, including padding.
1469    pub fn allocation_bytes(&self) -> u64 {
1470        self.allocation.len() as u64
1471    }
1472
1473    pub fn byte_offset(&self) -> u64 {
1474        self.byte_offset
1475    }
1476
1477    pub fn view_bytes(&self) -> u64 {
1478        self.view_bytes
1479    }
1480}
1481
1482impl<T> Clone for DeviceMemoryView<T> {
1483    fn clone(&self) -> Self {
1484        Self {
1485            ptr: self.ptr,
1486            len: self.len,
1487            storage: Arc::clone(&self.storage),
1488            element: std::marker::PhantomData,
1489        }
1490    }
1491}
1492
1493impl<T> DeviceMemoryView<T> {
1494    pub fn len(&self) -> usize {
1495        self.len
1496    }
1497    pub fn is_empty(&self) -> bool {
1498        self.len == 0
1499    }
1500    pub fn stream(&self) -> &Arc<CudaStream> {
1501        &self.storage.stream
1502    }
1503    pub fn device_ptr(&self) -> &u64 {
1504        &self.ptr
1505    }
1506
1507    /// Preserve this view's actual native allocation origin. A foreign owner
1508    /// supplies only its imported span, not proof of a complete allocation;
1509    /// never manufacture a native allocation identity for that case.
1510    pub fn allocation_provenance(&self) -> Option<DeviceAllocationProvenance> {
1511        let allocation = match &*self.storage.backing {
1512            Backing::Native(allocation) => allocation,
1513            Backing::Runtime(block) => block.allocation.as_ref()?,
1514            Backing::Foreign { .. } => return None,
1515        };
1516        let byte_offset = self.ptr.checked_sub(allocation.ptr())?;
1517        let view_bytes = u64::try_from(self.len.checked_mul(std::mem::size_of::<T>())?).ok()?;
1518        let allocation_bytes = u64::try_from(allocation.len()).ok()?;
1519        if byte_offset.checked_add(view_bytes)? > allocation_bytes {
1520            return None;
1521        }
1522        Some(DeviceAllocationProvenance {
1523            allocation: Arc::clone(allocation),
1524            byte_offset,
1525            view_bytes,
1526        })
1527    }
1528
1529    /// Complete byte view of the same actual native allocation. Callers still
1530    /// have to provide the access grant and producer/consumer ordering; physical
1531    /// ownership alone does not authorize use of these bytes.
1532    pub(crate) fn allocation_view(&self) -> Option<DeviceMemoryView<u8>> {
1533        let provenance = self.allocation_provenance()?;
1534        Some(DeviceMemoryView {
1535            ptr: provenance.allocation.ptr(),
1536            len: provenance.allocation.len(),
1537            storage: Arc::clone(&self.storage),
1538            element: std::marker::PhantomData,
1539        })
1540    }
1541
1542    pub fn try_slice(&self, range: impl std::ops::RangeBounds<usize>) -> Option<Self> {
1543        use std::ops::Bound;
1544        let start = match range.start_bound() {
1545            Bound::Included(&n) => n,
1546            Bound::Excluded(&n) => n.checked_add(1)?,
1547            Bound::Unbounded => 0,
1548        };
1549        let end = match range.end_bound() {
1550            Bound::Included(&n) => n.checked_add(1)?,
1551            Bound::Excluded(&n) => n,
1552            Bound::Unbounded => self.len,
1553        };
1554        if start > end || end > self.len {
1555            return None;
1556        }
1557        let offset = start.checked_mul(std::mem::size_of::<T>())?;
1558        Some(Self {
1559            ptr: self.ptr.checked_add(u64::try_from(offset).ok()?)?,
1560            len: end - start,
1561            storage: Arc::clone(&self.storage),
1562            element: std::marker::PhantomData,
1563        })
1564    }
1565
1566    pub fn slice(&self, range: impl std::ops::RangeBounds<usize>) -> Self {
1567        self.try_slice(range)
1568            .expect("device slice range is out of bounds")
1569    }
1570
1571    pub fn slice_mut(&mut self, range: impl std::ops::RangeBounds<usize>) -> Self {
1572        self.slice(range)
1573    }
1574
1575    /// Reinterpret the span without changing its allocation owner.
1576    ///
1577    /// # Safety
1578    /// The initialized device bytes must have the representation required by U
1579    /// before any read through the returned view. Alignment and size are checked.
1580    pub unsafe fn cast<U>(&self) -> Option<DeviceMemoryView<U>> {
1581        let bytes = self.len.checked_mul(std::mem::size_of::<T>())?;
1582        let size = std::mem::size_of::<U>();
1583        if size == 0
1584            || !bytes.is_multiple_of(size)
1585            || !self.ptr.is_multiple_of(std::mem::align_of::<U>() as u64)
1586        {
1587            return None;
1588        }
1589        Some(DeviceMemoryView {
1590            ptr: self.ptr,
1591            len: bytes / size,
1592            storage: Arc::clone(&self.storage),
1593            element: std::marker::PhantomData,
1594        })
1595    }
1596}
1597
1598impl<T: DeviceRepr + 'static> DeviceMemoryView<T> {
1599    /// Allocate through the same native owner used by runtime resources.
1600    pub(crate) fn allocate(
1601        stream: Arc<CudaStream>,
1602        allocation_stream: Arc<CudaStream>,
1603        len: usize,
1604    ) -> crate::device_runtime::ResourceResult<Self> {
1605        let bytes = len
1606            .checked_mul(std::mem::size_of::<T>())
1607            .ok_or_else(|| ResourceError::Driver("allocation size overflow".into()))?;
1608        let allocation = RawDeviceAllocation::allocate(
1609            Arc::clone(&stream),
1610            allocation_stream,
1611            bytes,
1612            None,
1613            Arc::default(),
1614        )?;
1615        let ptr = allocation.ptr();
1616        Ok(Self {
1617            ptr,
1618            len,
1619            storage: DeviceStorage::new(Backing::Native(allocation), stream, ptr),
1620            element: std::marker::PhantomData,
1621        })
1622    }
1623}
1624
1625impl<T> DeviceSlice<T> for DeviceMemoryView<T> {
1626    fn len(&self) -> usize {
1627        self.len
1628    }
1629    fn stream(&self) -> &Arc<CudaStream> {
1630        &self.storage.stream
1631    }
1632}
1633
1634/// Safe XLOG device operations accept owner-bearing spans, not native pointer
1635/// adapters. The returned span must retain the actual allocation.
1636pub trait DeviceRead<T>: private_access::Sealed {
1637    fn device_view(&self) -> DeviceMemoryView<T>;
1638}
1639
1640/// A span accepted as a destination by safe XLOG operations. Conflicting aliases
1641/// are excluded by operation admission, including aliases of a different type.
1642pub trait DeviceWrite<T>: DeviceRead<T> {}
1643
1644mod private_access {
1645    pub trait Sealed {}
1646}
1647
1648impl<T: DeviceRepr> private_access::Sealed for TrackedCudaSlice<T> {}
1649impl<T: DeviceRepr> DeviceRead<T> for TrackedCudaSlice<T> {
1650    fn device_view(&self) -> DeviceMemoryView<T> {
1651        self.view()
1652    }
1653}
1654impl<T: DeviceRepr> DeviceWrite<T> for TrackedCudaSlice<T> {}
1655impl<T> private_access::Sealed for DeviceMemoryView<T> {}
1656impl<T> DeviceRead<T> for DeviceMemoryView<T> {
1657    fn device_view(&self) -> DeviceMemoryView<T> {
1658        self.clone()
1659    }
1660}
1661impl<T> DeviceWrite<T> for DeviceMemoryView<T> {}
1662
1663impl<T: DeviceRepr> AsKernelParam for &DeviceMemoryView<T> {
1664    fn as_kernel_param(&self) -> *mut std::ffi::c_void {
1665        (&self.ptr as *const u64).cast_mut().cast()
1666    }
1667}
1668
1669impl<T: DeviceRepr> AsKernelParam for &mut DeviceMemoryView<T> {
1670    fn as_kernel_param(&self) -> *mut std::ffi::c_void {
1671        (&self.ptr as *const u64).cast_mut().cast()
1672    }
1673}
1674
1675impl<'a, T: DeviceRepr> IntoKernelParamStorage for &'a DeviceMemoryView<T> {
1676    type Storage = DeviceParamStorage<'a>;
1677
1678    fn into_kernel_param_storage(self) -> Self::Storage {
1679        DeviceParamStorage::unsynced(self.ptr)
1680    }
1681}
1682
1683impl<'a, T: DeviceRepr> IntoKernelParamStorage for &'a mut DeviceMemoryView<T> {
1684    type Storage = DeviceParamStorage<'a>;
1685
1686    fn into_kernel_param_storage(self) -> Self::Storage {
1687        DeviceParamStorage::unsynced(self.ptr)
1688    }
1689}
1690
1691/// One checked span and its real backing owner. Obtaining this manifest is
1692/// passive; only admission below grants access on the operation's stream.
1693#[derive(Clone)]
1694pub(crate) struct DeviceMemoryAccess {
1695    storage: Arc<DeviceStorage>,
1696    range: MemoryUse,
1697}
1698
1699impl DeviceMemoryAccess {
1700    pub(crate) fn retained_owner(&self) -> RetainedStorageUse {
1701        RetainedStorageUse {
1702            owner: self.storage.clone(),
1703            access: self.range.access(),
1704        }
1705    }
1706
1707    pub(crate) fn context(&self) -> usize {
1708        self.storage.stream.context().cu_ctx() as usize
1709    }
1710}
1711
1712impl<T> DeviceMemoryView<T> {
1713    pub(crate) fn access(
1714        &self,
1715        access: Access,
1716    ) -> crate::device_runtime::ResourceResult<DeviceMemoryAccess> {
1717        let bytes = self
1718            .len
1719            .checked_mul(std::mem::size_of::<T>())
1720            .ok_or_else(|| ResourceError::StreamMisuse("device view byte size overflow".into()))?;
1721        Ok(DeviceMemoryAccess {
1722            storage: Arc::clone(&self.storage),
1723            range: MemoryUse::new(self.ptr, bytes, access)?,
1724        })
1725    }
1726}
1727
1728/// Passive storage ownership. A graph retains this immutable manifest, never
1729/// an executing stream pin, access reservation, or prepared dependency state.
1730#[derive(Default)]
1731pub(crate) struct MemoryAccessManifest {
1732    accesses: Vec<DeviceMemoryAccess>,
1733    ranges: Vec<(usize, MemoryUse)>,
1734    retained: Vec<RetainedStorageUse>,
1735    runtimes: Vec<Arc<XlogDeviceRuntime>>,
1736    runtime_dependencies: Vec<(Arc<DeviceAccessDependencies>, Access)>,
1737    runtime_allocations: Vec<Arc<RawDeviceAllocation>>,
1738}
1739
1740impl MemoryAccessManifest {
1741    pub(crate) fn combine(manifests: &[Arc<Self>]) -> Arc<Self> {
1742        let mut combined = Self::default();
1743        for manifest in manifests {
1744            combined.accesses.extend(manifest.accesses.iter().cloned());
1745            combined.ranges.extend_from_slice(&manifest.ranges);
1746            combined
1747                .retained
1748                .extend(manifest.retained.iter().map(|retained| RetainedStorageUse {
1749                    owner: Arc::clone(&retained.owner),
1750                    access: retained.access,
1751                }));
1752            combined.runtimes.extend(manifest.runtimes.iter().cloned());
1753            combined
1754                .runtime_dependencies
1755                .extend(manifest.runtime_dependencies.iter().cloned());
1756            combined
1757                .runtime_allocations
1758                .extend(manifest.runtime_allocations.iter().cloned());
1759        }
1760        Arc::new(combined)
1761    }
1762
1763    pub(crate) fn covers(&self, ranges: &[(usize, MemoryUse)]) -> bool {
1764        ranges.iter().all(|(context, range)| {
1765            self.ranges
1766                .iter()
1767                .any(|(owned_context, owned)| context == owned_context && owned.covers(*range))
1768        })
1769    }
1770
1771    pub(crate) fn covered_by(&self, other: &Self) -> bool {
1772        other.covers(&self.ranges)
1773    }
1774}
1775
1776/// Positive completion of one admitted, synchronous enqueue callback and all
1777/// its nested submissions. This proof is never reused by another admission.
1778/// Only a successful wait on its exact execution can publish completion.
1779#[derive(Debug)]
1780pub(crate) struct OperationCompletion {
1781    execution_id: u64,
1782    complete: std::sync::atomic::AtomicBool,
1783}
1784
1785impl OperationCompletion {
1786    pub(crate) fn new(execution_id: u64) -> Self {
1787        Self {
1788            execution_id,
1789            complete: std::sync::atomic::AtomicBool::new(false),
1790        }
1791    }
1792
1793    pub(crate) fn validate_submission(
1794        &self,
1795        execution_id: u64,
1796    ) -> crate::device_runtime::ResourceResult<()> {
1797        if execution_id != self.execution_id || self.is_complete() {
1798            return Err(ResourceError::StreamMisuse(
1799                "memory operation requires its original uncompleted stream execution".into(),
1800            ));
1801        }
1802        Ok(())
1803    }
1804
1805    pub(crate) fn synchronize_with(
1806        &self,
1807        execution_id: u64,
1808        synchronize: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
1809    ) -> crate::device_runtime::ResourceResult<()> {
1810        if execution_id != self.execution_id {
1811            return Err(ResourceError::StreamMisuse(
1812                "memory operation cannot synchronize another stream execution".into(),
1813            ));
1814        }
1815        synchronize()?;
1816        self.complete.store(true, Ordering::Release);
1817        Ok(())
1818    }
1819
1820    pub(crate) fn is_complete(&self) -> bool {
1821        self.complete.load(Ordering::Acquire)
1822    }
1823
1824    /// A cold barrier on the owning context completes this closed admission,
1825    /// including work on an originating thread that has since exited. This is
1826    /// not a stream-identity substitution and cannot authorize another enqueue.
1827    fn synchronize_retired_with(
1828        &self,
1829        synchronize_context: impl FnOnce() -> crate::device_runtime::ResourceResult<()>,
1830    ) -> crate::device_runtime::ResourceResult<()> {
1831        if !self.is_complete() {
1832            synchronize_context()?;
1833            self.complete.store(true, Ordering::Release);
1834        }
1835        Ok(())
1836    }
1837}
1838
1839pub(crate) struct MemoryOperationOwner {
1840    stream: Arc<CudaStream>,
1841    manifest: Arc<MemoryAccessManifest>,
1842    retained: Vec<RetainedStorageUse>,
1843    captured: std::sync::atomic::AtomicBool,
1844    dependencies: std::sync::OnceLock<Vec<(Arc<DeviceAccessDependencies>, Access)>>,
1845    completion: Arc<OperationCompletion>,
1846}
1847
1848impl crate::launch::RecorderCleanup<MemoryUseGroup> for MemoryOperationOwner {
1849    fn synchronize_retired(&self) -> crate::device_runtime::ResourceResult<()> {
1850        if self.captured.load(Ordering::Acquire) {
1851            // Captured nodes retain their own storage; this admission submitted
1852            // no live work and must not synchronize or end that capture.
1853            return Ok(());
1854        }
1855        let _ordinary = crate::cuda_graph::reserve_capture_exclusion()?;
1856        self.completion.synchronize_retired_with(|| {
1857            // Failed cleanup may reach this queue after the original PTDS host
1858            // thread has exited and without a successfully recorded event. Only
1859            // a barrier on the retained context can cover that missing fence.
1860            // This potentially blocking wait belongs exclusively to cold error
1861            // retirement; normal enqueue/commit adds no context-wide wait.
1862            self.stream.context().bind_to_thread()?;
1863            self.stream.context().synchronize().map_err(|error| {
1864                ResourceError::Driver(format!(
1865                    "retired memory operation context wait failed: {error}"
1866                ))
1867            })
1868        })
1869    }
1870
1871    fn cancel_retired(&self, group: MemoryUseGroup) -> crate::device_runtime::ResourceResult<()> {
1872        self.cancel(&[group])
1873    }
1874}
1875
1876impl MemoryOperationOwner {
1877    pub(crate) fn stream(&self) -> &Arc<CudaStream> {
1878        &self.stream
1879    }
1880
1881    pub(crate) fn manifest(&self) -> &Arc<MemoryAccessManifest> {
1882        &self.manifest
1883    }
1884
1885    pub(crate) fn completion(&self) -> &Arc<OperationCompletion> {
1886        &self.completion
1887    }
1888
1889    pub(crate) fn bind_submission(
1890        self: &Arc<Self>,
1891        pin: &crate::cuda_graph::StreamSubmissionPin<'_>,
1892    ) -> crate::device_runtime::ResourceResult<()> {
1893        // Check before dependency preparation, including PTDS migration between
1894        // admission and enqueue. Cleanup must never certify a different thread.
1895        self.completion
1896            .validate_submission(pin.validate_execution(&self.stream)?)?;
1897        // Retain actual storage before the first node can reach the driver.
1898        // No persistent owner retains the submission pin or its capture target.
1899        if pin.capture_memory(&self.manifest) {
1900            self.captured.store(true, Ordering::Release);
1901        }
1902        Ok(())
1903    }
1904
1905    pub(crate) fn synchronize(&self) -> crate::device_runtime::ResourceResult<()> {
1906        if self.captured.load(Ordering::Acquire) {
1907            // Capture queued graph nodes, not live work. Its actual owners were
1908            // transferred before enqueue; only the capture owner may end it.
1909            return Ok(());
1910        }
1911        self.completion.synchronize_with(
1912            crate::cuda_graph::stream_execution_id(&self.stream)?,
1913            || {
1914                self.stream.synchronize().map_err(|error| {
1915                    ResourceError::Driver(format!(
1916                        "device memory operation synchronization failed: {error}"
1917                    ))
1918                })
1919            },
1920        )
1921    }
1922
1923    pub(crate) fn cancel(
1924        &self,
1925        groups: &[MemoryUseGroup],
1926    ) -> crate::device_runtime::ResourceResult<()> {
1927        let [group] = groups else {
1928            return Err(ResourceError::StreamMisuse(
1929                "device memory operation must own one exact reservation".into(),
1930            ));
1931        };
1932        block_use_registry()
1933            .lock()
1934            .expect("device block-use registry poisoned")
1935            .release_memory_uses(*group)
1936    }
1937
1938    pub(crate) fn prepare(&self) -> crate::device_runtime::ResourceResult<()> {
1939        if self.captured.load(Ordering::Acquire) {
1940            return Ok(());
1941        }
1942        let mut dependencies = Vec::new();
1943        for (dependency, access) in &self.manifest.runtime_dependencies {
1944            if let Some((_, existing_access)) = dependencies
1945                .iter_mut()
1946                .find(|(existing, _)| Arc::ptr_eq(existing, dependency))
1947            {
1948                *existing_access = crate::launch::combine_access(*existing_access, *access);
1949            } else {
1950                dependencies.push((Arc::clone(dependency), *access));
1951            }
1952        }
1953        for retained in &self.retained {
1954            let dependency = retained.owner.dependencies()?;
1955            if let Some((_, access)) = dependencies
1956                .iter_mut()
1957                .find(|(existing, _)| Arc::ptr_eq(existing, &dependency))
1958            {
1959                *access = crate::launch::combine_access(*access, retained.access);
1960            } else {
1961                dependencies.push((dependency, retained.access));
1962            }
1963        }
1964        self.dependencies.set(dependencies).map_err(|_| {
1965            ResourceError::StreamMisuse(
1966                "device memory dependencies may be prepared only once".into(),
1967            )
1968        })?;
1969        for (dependency, access) in self.dependencies.get().expect("dependencies installed") {
1970            dependency.prepare(&self.stream, *access)?;
1971        }
1972        Ok(())
1973    }
1974
1975    pub(crate) fn finish(
1976        &self,
1977        group: MemoryUseGroup,
1978    ) -> crate::device_runtime::ResourceResult<()> {
1979        if self.captured.load(Ordering::Acquire) {
1980            return self.cancel(&[group]);
1981        }
1982        // Keep the reservation until every overlapping allocation/import owner
1983        // has the completion dependency. A partial publication failure is
1984        // handled by the same synchronize/cancel/quarantine path as launches.
1985        let dependencies = self.dependencies.get().ok_or_else(|| {
1986            ResourceError::StreamMisuse(
1987                "device memory completion requires prepared dependencies".into(),
1988            )
1989        })?;
1990        DeviceAccessDependencies::record_operation_completion(
1991            dependencies,
1992            Arc::clone(&self.stream),
1993        )?;
1994        self.cancel(&[group])
1995    }
1996}
1997
1998/// Native pointers are available only while this private proof is borrowed by
1999/// the enqueue closure. It cannot be constructed from a raw address or kept
2000/// after the enclosing operation has published or cancelled its reservation.
2001pub(crate) struct MemoryEnqueue<'a> {
2002    owner: &'a Arc<MemoryOperationOwner>,
2003    submission: &'a crate::cuda_graph::StreamSubmissionPin<'a>,
2004}
2005
2006impl MemoryEnqueue<'_> {
2007    pub(crate) fn cuda_enqueue(
2008        &self,
2009    ) -> crate::device_runtime::ResourceResult<crate::launch::CudaEnqueue<'_>> {
2010        crate::launch::CudaEnqueue::from_admission(self.owner, self.submission)
2011    }
2012
2013    fn validate<T>(
2014        &self,
2015        view: &DeviceMemoryView<T>,
2016        access: Access,
2017    ) -> crate::device_runtime::ResourceResult<()> {
2018        let range = view.access(access)?.range;
2019        if !self.owner.manifest.accesses.iter().any(|admitted| {
2020            Arc::ptr_eq(&admitted.storage, &view.storage) && admitted.range.covers(range)
2021        }) {
2022            return Err(ResourceError::StreamMisuse(
2023                "native device view is not covered by the admitted access manifest".into(),
2024            ));
2025        }
2026        Ok(())
2027    }
2028
2029    pub(crate) fn read<'a, T>(
2030        &'a self,
2031        view: &'a DeviceMemoryView<T>,
2032    ) -> crate::device_runtime::ResourceResult<PreparedDeviceRead<'a, T>> {
2033        self.validate(view, Access::Read)?;
2034        self.validate_native_context(view)?;
2035        Ok(PreparedDeviceRead { view, proof: self })
2036    }
2037
2038    pub(crate) fn write<'a, T>(
2039        &'a self,
2040        view: &'a DeviceMemoryView<T>,
2041    ) -> crate::device_runtime::ResourceResult<PreparedDeviceWrite<'a, T>> {
2042        self.validate(view, Access::Write)?;
2043        self.validate_native_context(view)?;
2044        Ok(PreparedDeviceWrite { view, proof: self })
2045    }
2046
2047    fn validate_native_context<T>(
2048        &self,
2049        view: &DeviceMemoryView<T>,
2050    ) -> crate::device_runtime::ResourceResult<()> {
2051        if view.stream().context().cu_ctx() != self.owner.stream.context().cu_ctx() {
2052            return Err(ResourceError::StreamMisuse(
2053                "native device access requires the allocation context".into(),
2054            ));
2055        }
2056        Ok(())
2057    }
2058
2059    pub(crate) fn copy<T>(
2060        &self,
2061        source: &DeviceMemoryView<T>,
2062        destination: &DeviceMemoryView<T>,
2063    ) -> crate::device_runtime::ResourceResult<()> {
2064        self.validate(source, Access::Read)?;
2065        self.validate(destination, Access::Write)?;
2066        self.validate_native_context(destination)?;
2067        let source_context = source.stream().context().cu_ctx();
2068        let destination_context = destination.stream().context().cu_ctx();
2069        source.access(Access::Read)?.range.validate_copy_to(
2070            source_context as usize,
2071            destination.access(Access::Write)?.range,
2072            destination_context as usize,
2073        )?;
2074        if source_context == destination_context {
2075            self.owner
2076                .stream
2077                .memcpy_dtod(&self.read(source)?, &mut self.write(destination)?)?;
2078        } else {
2079            self.owner.stream.context().bind_to_thread()?;
2080            // SAFETY: the one admitted operation retains both exact allocation
2081            // contexts and byte ranges, with a source read and destination write.
2082            // All prior dependencies were queued before this proof was borrowed.
2083            // The operation's stream belongs to the destination context.
2084            unsafe {
2085                cudarc::driver::result::memcpy_peer_async(
2086                    destination_context,
2087                    destination.ptr,
2088                    source_context,
2089                    source.ptr,
2090                    source
2091                        .len
2092                        .checked_mul(std::mem::size_of::<T>())
2093                        .ok_or_else(|| {
2094                            ResourceError::StreamMisuse("device copy byte size overflow".into())
2095                        })?,
2096                    self.owner.stream.cu_stream(),
2097                )?;
2098            }
2099        }
2100        Ok(())
2101    }
2102}
2103
2104pub(crate) struct PreparedDeviceRead<'a, T> {
2105    view: &'a DeviceMemoryView<T>,
2106    proof: &'a MemoryEnqueue<'a>,
2107}
2108
2109pub(crate) struct PreparedDeviceWrite<'a, T> {
2110    view: &'a DeviceMemoryView<T>,
2111    proof: &'a MemoryEnqueue<'a>,
2112}
2113
2114impl<T> DeviceSlice<T> for PreparedDeviceRead<'_, T> {
2115    fn len(&self) -> usize {
2116        self.view.len
2117    }
2118    fn stream(&self) -> &Arc<CudaStream> {
2119        self.view.stream()
2120    }
2121}
2122
2123impl<T> DevicePtr<T> for PreparedDeviceRead<'_, T> {
2124    fn device_ptr<'a>(&'a self, stream: &'a CudaStream) -> (u64, SyncOnDrop<'a>) {
2125        assert!(
2126            std::ptr::eq(stream, self.proof.owner.stream.as_ref()),
2127            "prepared view used on a different stream"
2128        );
2129        (self.view.ptr, SyncOnDrop::Sync(None))
2130    }
2131}
2132
2133impl<T> DeviceSlice<T> for PreparedDeviceWrite<'_, T> {
2134    fn len(&self) -> usize {
2135        self.view.len
2136    }
2137    fn stream(&self) -> &Arc<CudaStream> {
2138        self.view.stream()
2139    }
2140}
2141
2142impl<T> DevicePtr<T> for PreparedDeviceWrite<'_, T> {
2143    fn device_ptr<'a>(&'a self, stream: &'a CudaStream) -> (u64, SyncOnDrop<'a>) {
2144        assert!(
2145            std::ptr::eq(stream, self.proof.owner.stream.as_ref()),
2146            "prepared view used on a different stream"
2147        );
2148        (self.view.ptr, SyncOnDrop::Sync(None))
2149    }
2150}
2151
2152impl<T> DevicePtrMut<T> for PreparedDeviceWrite<'_, T> {
2153    fn device_ptr_mut<'a>(&'a mut self, stream: &'a CudaStream) -> (u64, SyncOnDrop<'a>) {
2154        assert!(
2155            std::ptr::eq(stream, self.proof.owner.stream.as_ref()),
2156            "prepared view used on a different stream"
2157        );
2158        (self.view.ptr, SyncOnDrop::Sync(None))
2159    }
2160}
2161
2162struct MemoryOperation {
2163    transaction: RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>,
2164    // Host admission is not part of the storage quarantine on uncertain free.
2165    submission: crate::cuda_graph::StreamSubmissionPhase,
2166}
2167
2168impl Drop for MemoryOperation {
2169    fn drop(&mut self) {
2170        if self.transaction.is_prepared() {
2171            // abort_with retains the whole owner capsule if synchronization or
2172            // cancellation fails, including on unwinding from the enqueue.
2173            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2174                self.transaction.abort_with(
2175                    MemoryOperationOwner::synchronize,
2176                    MemoryOperationOwner::cancel,
2177                )
2178            }));
2179        }
2180    }
2181}
2182
2183/// One admission path for copies and recorded launches. No driver wait or
2184/// publication runs under the registry mutex; source and alias owners are
2185/// transferred to the armed transaction before that mutex is released.
2186pub(crate) fn admit_memory_access(
2187    stream: Arc<CudaStream>,
2188    accesses: Vec<DeviceMemoryAccess>,
2189    runtime: Option<Arc<XlogDeviceRuntime>>,
2190    runtime_uses: &[crate::device_runtime::BlockUse],
2191    execution_id: u64,
2192) -> crate::device_runtime::ResourceResult<RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>>
2193{
2194    let mut ranges_by_context = std::collections::BTreeMap::<usize, Vec<MemoryUse>>::new();
2195    for access in &accesses {
2196        ranges_by_context
2197            .entry(access.context())
2198            .or_default()
2199            .push(access.range);
2200    }
2201    if let Some(runtime) = &runtime {
2202        let context = runtime.device().inner().stream().context().cu_ctx() as usize;
2203        for use_ in runtime_uses {
2204            ranges_by_context
2205                .entry(context)
2206                .or_default()
2207                .push(MemoryUse::new(use_.block.ptr, use_.bytes, use_.access)?);
2208        }
2209    } else if !runtime_uses.is_empty() {
2210        return Err(ResourceError::StreamMisuse(
2211            "recorded blocks require their runtime owner".into(),
2212        ));
2213    }
2214    let ranges = ranges_by_context
2215        .iter()
2216        .flat_map(|(context, ranges)| ranges.iter().map(|range| (*context, *range)))
2217        .collect::<Vec<_>>();
2218    let mut manifest = MemoryAccessManifest {
2219        // Even an empty span retains its actual owner; weak overlap discovery
2220        // is an alias rendezvous, not proof that the source is owned.
2221        retained: accesses
2222            .iter()
2223            .map(DeviceMemoryAccess::retained_owner)
2224            .collect(),
2225        accesses,
2226        ranges,
2227        runtimes: runtime.iter().cloned().collect(),
2228        runtime_dependencies: Vec::new(),
2229        runtime_allocations: Vec::new(),
2230    };
2231    let mut registry = block_use_registry()
2232        .lock()
2233        .expect("device block-use registry poisoned");
2234    if let Some(runtime) = &runtime {
2235        for use_ in runtime_uses {
2236            let dependencies = runtime
2237                .allocation_dependencies(use_.block, use_.bytes)?
2238                .ok_or_else(|| {
2239                    ResourceError::StreamMisuse(
2240                        "recorded block backend does not expose allocation dependencies".into(),
2241                    )
2242                })?;
2243            manifest
2244                .runtime_allocations
2245                .push(dependencies.retain_allocation()?);
2246            manifest
2247                .runtime_dependencies
2248                .push((dependencies, use_.access));
2249        }
2250    }
2251    for (context, ranges) in &ranges_by_context {
2252        registry.retain_storage_uses(*context, ranges, &mut manifest.retained)?;
2253    }
2254    let manifest = Arc::new(manifest);
2255    // Owner discovery and range reservation share this registry lock. A fresh
2256    // manifest therefore needs no second alias scan during admission.
2257    let group = reserve_memory_manifest_locked(&mut registry, &manifest);
2258    drop(registry);
2259    let retained = manifest
2260        .retained
2261        .iter()
2262        .map(|retained| RetainedStorageUse {
2263            owner: Arc::clone(&retained.owner),
2264            access: retained.access,
2265        })
2266        .collect();
2267    memory_operation_transaction(stream, manifest, retained, execution_id, group?)
2268}
2269
2270pub(crate) fn admit_memory_manifest(
2271    stream: Arc<CudaStream>,
2272    manifest: Arc<MemoryAccessManifest>,
2273    execution_id: u64,
2274) -> crate::device_runtime::ResourceResult<RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>>
2275{
2276    let mut retained = manifest
2277        .retained
2278        .iter()
2279        .map(|retained| RetainedStorageUse {
2280            owner: Arc::clone(&retained.owner),
2281            access: retained.access,
2282        })
2283        .collect::<Vec<_>>();
2284    let mut registry = block_use_registry()
2285        .lock()
2286        .expect("device block-use registry poisoned");
2287    // Passive manifests may outlive their original admission. Refresh aliases
2288    // once, then reserve their complete ranges under this same registry lock.
2289    let mut ranges_by_context = std::collections::BTreeMap::<usize, Vec<MemoryUse>>::new();
2290    for (context, range) in &manifest.ranges {
2291        ranges_by_context.entry(*context).or_default().push(*range);
2292    }
2293    for (context, ranges) in ranges_by_context {
2294        registry.retain_storage_uses(context, &ranges, &mut retained)?;
2295    }
2296    let group = reserve_memory_manifest_locked(&mut registry, &manifest);
2297    drop(registry);
2298    memory_operation_transaction(stream, manifest, retained, execution_id, group?)
2299}
2300
2301fn reserve_memory_manifest_locked(
2302    registry: &mut BlockUseRegistry,
2303    manifest: &MemoryAccessManifest,
2304) -> crate::device_runtime::ResourceResult<MemoryUseGroup> {
2305    let source_proofs = manifest
2306        .accesses
2307        .iter()
2308        .map(|access| access.storage.dependencies.reclamation.release_proof())
2309        .chain(
2310            manifest
2311                .runtime_dependencies
2312                .iter()
2313                .map(|(deps, _)| deps.reclamation.release_proof()),
2314        )
2315        .collect::<Vec<_>>();
2316    registry.reserve_owned_memory_uses(&manifest.ranges, &source_proofs)
2317}
2318
2319fn memory_operation_transaction(
2320    stream: Arc<CudaStream>,
2321    manifest: Arc<MemoryAccessManifest>,
2322    retained: Vec<RetainedStorageUse>,
2323    execution_id: u64,
2324    group: MemoryUseGroup,
2325) -> crate::device_runtime::ResourceResult<RecorderTransaction<MemoryOperationOwner, MemoryUseGroup>>
2326{
2327    let owner = Arc::new(MemoryOperationOwner {
2328        stream,
2329        manifest,
2330        retained,
2331        captured: std::sync::atomic::AtomicBool::new(false),
2332        dependencies: std::sync::OnceLock::new(),
2333        completion: Arc::new(OperationCompletion::new(execution_id)),
2334    });
2335    Ok(RecorderTransaction::from_admitted(owner, Box::new([group])))
2336}
2337
2338pub(crate) fn with_memory_access<R>(
2339    stream: Arc<CudaStream>,
2340    accesses: Vec<DeviceMemoryAccess>,
2341    operation: impl FnOnce(&MemoryEnqueue<'_>) -> crate::device_runtime::ResourceResult<R>,
2342) -> crate::device_runtime::ResourceResult<R> {
2343    let submission = crate::cuda_graph::acquire_stream_submission_phase(&stream)?;
2344    let execution_id = submission.execution_id();
2345    let guard = MemoryOperation {
2346        transaction: admit_memory_access(stream, accesses, None, &[], execution_id)?,
2347        submission,
2348    };
2349    with_memory_operation(guard, |owner, submission| {
2350        operation(&MemoryEnqueue { owner, submission })
2351    })
2352}
2353
2354pub(crate) fn with_memory_manifest<T>(
2355    stream: Arc<CudaStream>,
2356    manifest: Arc<MemoryAccessManifest>,
2357    operation: impl FnOnce(&crate::launch::CudaEnqueue<'_>) -> crate::device_runtime::ResourceResult<T>,
2358) -> crate::device_runtime::ResourceResult<T> {
2359    let submission = crate::cuda_graph::acquire_stream_submission_phase(&stream)?;
2360    let execution_id = submission.execution_id();
2361    let guard = MemoryOperation {
2362        transaction: admit_memory_manifest(stream, manifest, execution_id)?,
2363        submission,
2364    };
2365    with_memory_operation(guard, |owner, pin| {
2366        operation(&crate::launch::CudaEnqueue::from_admission(owner, pin)?)
2367    })
2368}
2369
2370fn with_memory_operation<T>(
2371    mut guard: MemoryOperation,
2372    operation: impl FnOnce(
2373        &Arc<MemoryOperationOwner>,
2374        &crate::cuda_graph::StreamSubmissionPin<'_>,
2375    ) -> crate::device_runtime::ResourceResult<T>,
2376) -> crate::device_runtime::ResourceResult<T> {
2377    let owner = Arc::clone(
2378        guard
2379            .transaction
2380            .prepared_owner()
2381            .expect("memory operation admitted"),
2382    );
2383    let mut result = None;
2384    guard
2385        .transaction
2386        .enqueue_operation_with(
2387            || {
2388                let pin = guard.submission.pin()?;
2389                owner.bind_submission(&pin)?;
2390                Ok(pin)
2391            },
2392            MemoryOperationOwner::prepare,
2393            |pin| {
2394                result = Some(pin.with_serialized_submission(|| operation(&owner, pin))?);
2395                Ok::<_, ResourceError>(())
2396            },
2397            MemoryOperationOwner::synchronize,
2398            MemoryOperationOwner::cancel,
2399        )
2400        .map_err(|error| ResourceError::Driver(error.to_string()))?;
2401    guard.transaction.commit_with(
2402        MemoryOperationOwner::finish,
2403        MemoryOperationOwner::synchronize,
2404        MemoryOperationOwner::cancel,
2405    )?;
2406    Ok(result.expect("successful memory operation produced its result"))
2407}
2408
2409#[derive(Clone)]
2410pub(crate) struct RuntimeAllocationIdentity {
2411    pub(crate) manager_id: usize,
2412    pub(crate) allocation_ptr: u64,
2413    pub(crate) allocation_bytes: usize,
2414    pub(crate) block_id: BlockId,
2415    pub(crate) block_bytes: usize,
2416    pub(crate) block_state: BlockState,
2417    pub(crate) context: Arc<cudarc::driver::CudaContext>,
2418}
2419
2420impl<T: cudarc::driver::DeviceRepr> DeviceSlice<T> for TrackedCudaSlice<T> {
2421    fn len(&self) -> usize {
2422        self.len
2423    }
2424
2425    fn stream(&self) -> &Arc<CudaStream> {
2426        &self.storage.stream
2427    }
2428}
2429
2430impl<T: cudarc::driver::DeviceRepr> TrackedCudaSlice<T> {
2431    pub fn len(&self) -> usize {
2432        self.len
2433    }
2434
2435    pub fn is_empty(&self) -> bool {
2436        self.len == 0
2437    }
2438
2439    pub fn stream(&self) -> &Arc<CudaStream> {
2440        &self.storage.stream
2441    }
2442
2443    pub fn device_ptr(&self) -> &cudarc::driver::sys::CUdeviceptr {
2444        &self.ptr
2445    }
2446
2447    pub fn device_ptr_value(&self) -> cudarc::driver::sys::CUdeviceptr {
2448        self.ptr
2449    }
2450
2451    /// A passive view retaining this allocation; obtaining it performs no device access.
2452    pub fn view(&self) -> DeviceMemoryView<T> {
2453        DeviceMemoryView {
2454            ptr: self.ptr,
2455            len: self.len,
2456            storage: Arc::clone(&self.storage),
2457            element: std::marker::PhantomData,
2458        }
2459    }
2460
2461    /// Retain a disjoint typed span of this allocation as an owning slice.
2462    ///
2463    /// The returned slice shares the physical allocation and budget charge.
2464    /// Operation admission still applies to its exact byte range, so sibling
2465    /// spans can be recorded independently without duplicating ownership.
2466    pub(crate) fn try_owned_subslice<U: cudarc::driver::DeviceRepr>(
2467        &self,
2468        byte_range: std::ops::Range<usize>,
2469    ) -> Option<TrackedCudaSlice<U>> {
2470        let bytes = self.view().try_slice(byte_range)?;
2471        // SAFETY: callers receive an uninitialized device allocation span, as
2472        // they do from alloc::<U>. Alignment and extent are checked by cast;
2473        // the span must be initialized before any device read.
2474        let typed = unsafe { bytes.cast::<U>()? };
2475        Some(TrackedCudaSlice {
2476            storage: Arc::clone(&typed.storage),
2477            ptr: typed.ptr,
2478            len: typed.len,
2479            element: std::marker::PhantomData,
2480        })
2481    }
2482
2483    pub fn try_slice(
2484        &self,
2485        range: impl std::ops::RangeBounds<usize>,
2486    ) -> Option<DeviceMemoryView<T>> {
2487        self.view().try_slice(range)
2488    }
2489
2490    pub fn slice(&self, range: impl std::ops::RangeBounds<usize>) -> DeviceMemoryView<T> {
2491        self.try_slice(range)
2492            .expect("device slice range is out of bounds")
2493    }
2494
2495    pub fn slice_mut(&mut self, range: impl std::ops::RangeBounds<usize>) -> DeviceMemoryView<T> {
2496        self.slice(range)
2497    }
2498
2499    /// Stable address of the memory manager that owns this allocation.
2500    pub fn memory_manager_ptr_value(&self) -> usize {
2501        Arc::as_ptr(
2502            self.storage
2503                .manager()
2504                .expect("tracked allocation has a memory manager"),
2505        ) as usize
2506    }
2507
2508    pub(crate) fn runtime_allocation_identity(&self) -> Result<Option<RuntimeAllocationIdentity>> {
2509        let Some(block) = self.runtime_block() else {
2510            return Ok(None);
2511        };
2512        let allocation_bytes = self
2513            .len()
2514            .checked_mul(std::mem::size_of::<T>())
2515            .ok_or_else(|| XlogError::Kernel("runtime allocation byte size overflow".into()))?;
2516        Ok(Some(RuntimeAllocationIdentity {
2517            manager_id: self.memory_manager_ptr_value(),
2518            allocation_ptr: self.device_ptr_value(),
2519            allocation_bytes,
2520            block_id: BlockId::from_block(block),
2521            block_bytes: block.bytes,
2522            block_state: block.state,
2523            context: Arc::clone(DeviceSlice::stream(self).context()),
2524        }))
2525    }
2526
2527    /// Runtime block identity, when present. Device-native and foreign owners
2528    /// have no runtime block ID but still participate in shared range admission.
2529    pub fn runtime_block(&self) -> Option<&crate::device_runtime::DeviceBlock> {
2530        match &*self.storage.backing {
2531            Backing::Native(_) | Backing::Foreign { .. } => None,
2532            Backing::Runtime(allocation) => Some(allocation.device_block()),
2533        }
2534    }
2535
2536    /// Reinterpret this typed allocation as a raw byte allocation.
2537    ///
2538    /// This is a zero-copy conversion used by XLOG's columnar
2539    /// `CudaBuffer` representation, which stores device memory as
2540    /// untyped bytes + a schema. The conversion preserves the
2541    /// underlying [`Backing`] — runtime-routed slices remain
2542    /// runtime-routed, legacy cudarc slices remain cudarc-routed —
2543    /// so deallocation continues to match the original allocator.
2544    pub fn into_bytes(self) -> TrackedCudaSlice<u8> {
2545        let len = self
2546            .len
2547            .checked_mul(std::mem::size_of::<T>())
2548            .expect("tracked slice byte size must fit into usize");
2549        TrackedCudaSlice {
2550            storage: self.storage,
2551            ptr: self.ptr,
2552            len,
2553            element: std::marker::PhantomData,
2554        }
2555    }
2556}
2557
2558impl<T: cudarc::driver::DeviceRepr> AsKernelParam for &TrackedCudaSlice<T> {
2559    fn as_kernel_param(&self) -> *mut std::ffi::c_void {
2560        ((*self).device_ptr() as *const cudarc::driver::sys::CUdeviceptr)
2561            .cast_mut()
2562            .cast()
2563    }
2564}
2565
2566impl<T: cudarc::driver::DeviceRepr> AsKernelParam for &mut TrackedCudaSlice<T> {
2567    fn as_kernel_param(&self) -> *mut std::ffi::c_void {
2568        ((self.device_ptr()) as *const cudarc::driver::sys::CUdeviceptr)
2569            .cast_mut()
2570            .cast()
2571    }
2572}
2573
2574impl<'a, T: cudarc::driver::DeviceRepr> IntoKernelParamStorage for &'a TrackedCudaSlice<T> {
2575    type Storage = DeviceParamStorage<'a>;
2576
2577    fn into_kernel_param_storage(self) -> Self::Storage {
2578        DeviceParamStorage::unsynced(self.ptr)
2579    }
2580}
2581
2582impl<T: cudarc::driver::DeviceRepr> IntoKernelParamStorage for &mut TrackedCudaSlice<T> {
2583    type Storage = DeviceParamStorage<'static>;
2584
2585    fn into_kernel_param_storage(self) -> Self::Storage {
2586        DeviceParamStorage::unsynced(self.ptr)
2587    }
2588}
2589
2590impl GpuMemoryManager {
2591    /// Create a new GPU memory manager
2592    ///
2593    /// # Arguments
2594    /// * `device` - The CUDA device to allocate memory on
2595    /// * `budget` - Memory budget configuration
2596    pub(crate) fn new(device: Arc<CudaDevice>, budget: MemoryBudget) -> Self {
2597        Self {
2598            device,
2599            budget,
2600            accounting: Arc::new(GpuMemoryAccounting::default()),
2601            runtime: None,
2602            #[cfg(test)]
2603            after_local_reservation_hook: std::sync::Mutex::new(None),
2604        }
2605    }
2606
2607    /// Like [`new`], but additionally attaches a v0.6
2608    /// [`XlogDeviceRuntime`]. The runtime mediates **both**
2609    /// [`alloc::<T>`](Self::alloc) and [`alloc_raw`](Self::alloc_raw)
2610    /// through the v0.6 resource stack: typed `alloc::<T>` returns a
2611    /// [`TrackedCudaSlice<T>`] whose underlying memory is owned by
2612    /// the runtime (typed view via cudarc's `upgrade_device_ptr::<T>`,
2613    /// freed through the runtime on drop). The legacy cudarc path is
2614    /// only used when the manager is built via [`new`] (no runtime
2615    /// attached). Provider construction does not yet require the
2616    /// runtime; callers that want runtime-routed allocations opt in
2617    /// here.
2618    pub(crate) fn with_runtime(
2619        device: Arc<CudaDevice>,
2620        budget: MemoryBudget,
2621        runtime: Arc<XlogDeviceRuntime>,
2622    ) -> Self {
2623        Self {
2624            device,
2625            budget,
2626            accounting: Arc::new(GpuMemoryAccounting::default()),
2627            runtime: Some(runtime),
2628            #[cfg(test)]
2629            after_local_reservation_hook: std::sync::Mutex::new(None),
2630        }
2631    }
2632
2633    /// Atomically reserve `bytes` for one bounded multi-allocation request.
2634    ///
2635    /// The returned token owns the complete local-budget claim. A
2636    /// runtime-backed manager also reserves the same bytes against the
2637    /// runtime resource stack's finite global budget before touching local
2638    /// accounting; runtimes without a reservable global budget are refused.
2639    /// Creating the token performs no device allocation and does not increment
2640    /// [`alloc_count`](Self::alloc_count).
2641    pub fn reserve_bytes(self: &Arc<Self>, bytes: u64) -> Result<GpuMemoryReservation> {
2642        let runtime_reservation = match &self.runtime {
2643            Some(runtime) => {
2644                let bytes_usize = usize::try_from(bytes).map_err(|_| {
2645                    XlogError::Kernel(format!(
2646                        "GPU reservation size {} bytes exceeds platform usize",
2647                        bytes
2648                    ))
2649                })?;
2650                Some(runtime.reserve_memory(bytes_usize).map_err(|error| {
2651                    map_resource_error(error, self.accounting.peak.load(Ordering::SeqCst))
2652                })?)
2653            }
2654            None => None,
2655        };
2656        self.reserve_local_bytes(bytes, "manager_reserve")?;
2657        Ok(GpuMemoryReservation {
2658            manager: Arc::clone(self),
2659            runtime_reservation,
2660            total_bytes: bytes,
2661            remaining_bytes: bytes,
2662        })
2663    }
2664
2665    fn reserve_local_bytes(&self, bytes: u64, layer: &'static str) -> Result<()> {
2666        let _mutation = self
2667            .accounting
2668            .mutation_lock
2669            .lock()
2670            .expect("GPU memory accounting poisoned");
2671        loop {
2672            let current = self.accounting.budget_reserved.load(Ordering::SeqCst);
2673            let required = current as u128 + bytes as u128;
2674            if required > self.budget.device_bytes as u128 {
2675                return Err(MemoryPressure {
2676                    layer,
2677                    current_bytes: current as u128,
2678                    requested_bytes: bytes as u128,
2679                    budget_bytes: self.budget.device_bytes,
2680                    prior_peak_bytes: self.accounting.peak.load(Ordering::SeqCst),
2681                }
2682                .into_error());
2683            }
2684            if self
2685                .accounting
2686                .budget_reserved
2687                .compare_exchange(current, required as u64, Ordering::SeqCst, Ordering::SeqCst)
2688                .is_ok()
2689            {
2690                return Ok(());
2691            }
2692        }
2693    }
2694
2695    /// Borrow the attached device runtime, if any. `None` when the
2696    /// manager was constructed via [`new`].
2697    ///
2698    /// This accessor is the supported bridge for provider operations
2699    /// that receive runtime-owned allocations through the memory manager
2700    /// and must submit recorded work through
2701    /// [`crate::launch::LaunchRecorder`]. Code that already owns the
2702    /// runtime should use that `Arc<XlogDeviceRuntime>` directly.
2703    pub fn runtime(&self) -> Option<&Arc<XlogDeviceRuntime>> {
2704        self.runtime.as_ref()
2705    }
2706
2707    /// Complete stream-ordered frees that are pending in the attached device
2708    /// runtime.
2709    ///
2710    /// Diagnostic transactions use this after dropping temporary buffers on
2711    /// an error path so a later operation observes the restored byte budget.
2712    pub fn reap_pending_deallocations(&self) -> Result<()> {
2713        let Some(runtime) = self.runtime.as_ref() else {
2714            crate::cuda_graph::reap_capture_retirements();
2715            return Ok(());
2716        };
2717        runtime
2718            .reap_pending()
2719            .map_err(|error| map_resource_error(error, self.peak_bytes()))
2720    }
2721
2722    /// Release a local-budget reservation that was not admitted by the
2723    /// underlying allocator. Admitted accounting is untouched because the
2724    /// request was never published there.
2725    fn rollback_local_reservation(&self, bytes: u64) -> Result<()> {
2726        self.accounting.release_reserved(bytes)
2727    }
2728
2729    fn record_deallocation_failure(&self, bytes: u64) {
2730        let _ = self.accounting.deallocation_failure_count.try_update(
2731            Ordering::SeqCst,
2732            Ordering::SeqCst,
2733            |current| Some(current.saturating_add(1)),
2734        );
2735        let _ = self.accounting.deallocation_failure_bytes.try_update(
2736            Ordering::SeqCst,
2737            Ordering::SeqCst,
2738            |current| Some(current.saturating_add(bytes)),
2739        );
2740    }
2741
2742    /// Allocate `len` elements through the attached runtime or the shared raw
2743    /// allocator. Allocation completes before publication; retained failures
2744    /// keep their actual allocation and local byte claim.
2745    pub fn alloc<T: cudarc::driver::DeviceRepr>(
2746        self: &Arc<Self>,
2747        len: usize,
2748    ) -> Result<TrackedCudaSlice<T>> {
2749        // Count every device allocation request (resettable no-host-gate counter).
2750        self.accounting.alloc_count.fetch_add(1, Ordering::Relaxed);
2751
2752        // Fix Issue 2: Use checked_mul to prevent integer overflow before cast
2753        let bytes = (len as u64)
2754            .checked_mul(std::mem::size_of::<T>() as u64)
2755            .ok_or_else(|| XlogError::Kernel("Allocation size overflow".to_string()))?;
2756
2757        self.reserve_local_bytes(bytes, "manager_alloc")?;
2758        let attempt = LocalAllocationAttempt::new(Arc::clone(&self.accounting), bytes, None);
2759        self.alloc_after_local_reservation::<T>(len, bytes, None, Arc::clone(&attempt.reclamation))
2760            .map_err(|error| map_resource_error(error, self.peak_bytes()))
2761    }
2762
2763    fn alloc_after_local_reservation<T: cudarc::driver::DeviceRepr>(
2764        self: &Arc<Self>,
2765        len: usize,
2766        bytes: u64,
2767        runtime_reservation: Option<&mut RuntimeMemoryReservation>,
2768        reclamation: Arc<AllocationReclamation>,
2769    ) -> crate::device_runtime::ResourceResult<TrackedCudaSlice<T>> {
2770        let extent = usize::try_from(bytes)
2771            .map_err(|_| ResourceError::Driver("allocation size exceeds platform usize".into()))?;
2772        if self.runtime.is_some() && bytes != 0 {
2773            let allocation = self.alloc_raw_after_local_reservation(
2774                extent,
2775                bytes,
2776                AllocTag::UNTAGGED,
2777                runtime_reservation,
2778                reclamation,
2779            )?;
2780            let raw_ptr = allocation.device_block().ptr;
2781            return Ok(TrackedCudaSlice {
2782                storage: DeviceStorage::new(
2783                    Backing::Runtime(allocation),
2784                    Arc::clone(self.device.inner().stream()),
2785                    raw_ptr,
2786                ),
2787                ptr: raw_ptr,
2788                len,
2789                element: std::marker::PhantomData,
2790            });
2791        }
2792
2793        #[cfg(test)]
2794        self.run_after_local_reservation_hook(bytes);
2795        let allocation = RawDeviceAllocation::allocate(
2796            Arc::clone(self.device.inner().stream()),
2797            Arc::clone(self.device.inner().allocation_stream()),
2798            extent,
2799            Some(Arc::clone(self)),
2800            reclamation,
2801        )?;
2802        let raw_ptr = allocation.ptr();
2803        Ok(TrackedCudaSlice {
2804            storage: DeviceStorage::new(
2805                Backing::Native(allocation),
2806                Arc::clone(self.device.inner().stream()),
2807                raw_ptr,
2808            ),
2809            ptr: raw_ptr,
2810            len,
2811            element: std::marker::PhantomData,
2812        })
2813    }
2814
2815    /// Check if an allocation of `bytes` would exceed the budget
2816    ///
2817    /// # Arguments
2818    /// * `bytes` - Number of bytes to allocate
2819    ///
2820    /// # Returns
2821    /// `Ok(())` if allocation is within budget
2822    ///
2823    /// # Errors
2824    /// `XlogError::ResourceExhausted` if allocation would exceed budget
2825    pub fn check_budget(&self, bytes: u64) -> Result<()> {
2826        // Include provisional local reservations: this is a budget-admission
2827        // query, not a sample of already admitted allocations.
2828        let current = self.accounting.budget_reserved.load(Ordering::SeqCst);
2829        let required = current as u128 + bytes as u128;
2830
2831        if required > self.budget.device_bytes as u128 {
2832            return Err(MemoryPressure {
2833                layer: "manager_check_budget",
2834                current_bytes: current as u128,
2835                requested_bytes: bytes as u128,
2836                budget_bytes: self.budget.device_bytes,
2837                prior_peak_bytes: self.accounting.peak.load(Ordering::SeqCst),
2838            }
2839            .into_error());
2840        }
2841
2842        Ok(())
2843    }
2844
2845    /// Get the current allocated memory in bytes
2846    pub fn allocated_bytes(&self) -> u64 {
2847        self.accounting.allocated.load(Ordering::SeqCst)
2848    }
2849
2850    /// Number of runtime deallocations that returned an error. Their bytes
2851    /// remain charged locally because physical release was not proven.
2852    pub fn deallocation_failure_count(&self) -> u64 {
2853        self.accounting
2854            .deallocation_failure_count
2855            .load(Ordering::SeqCst)
2856    }
2857
2858    /// Total bytes retained in local accounting after runtime deallocation
2859    /// errors.
2860    pub fn deallocation_failure_bytes(&self) -> u64 {
2861        self.accounting
2862            .deallocation_failure_bytes
2863            .load(Ordering::SeqCst)
2864    }
2865
2866    /// High-water mark of successful manager-accounted reservations since
2867    /// construction or the last [`reset_peak`](Self::reset_peak). Direct CUDA
2868    /// allocations that bypass this manager are not included.
2869    pub fn peak_bytes(&self) -> u64 {
2870        self.accounting.peak.load(Ordering::SeqCst)
2871    }
2872
2873    /// Reset the peak high-water mark to the *current* allocated
2874    /// level, so a measurement window starts from live state rather
2875    /// than zero. Measurement-harness API.
2876    pub fn reset_peak(&self) {
2877        self.accounting.peak.store(
2878            self.accounting.allocated.load(Ordering::SeqCst),
2879            Ordering::SeqCst,
2880        );
2881    }
2882
2883    /// Number of `alloc` calls issued so far (device allocation requests).
2884    /// The GPU-resident MC engine snapshots this around the measured region to
2885    /// prove `per_operator_host_allocations == 0` (all arenas pre-allocated).
2886    pub fn alloc_count(&self) -> u64 {
2887        self.accounting.alloc_count.load(Ordering::Relaxed)
2888    }
2889
2890    /// Reset the allocation-request counter to zero.
2891    pub fn reset_alloc_count(&self) {
2892        self.accounting.alloc_count.store(0, Ordering::Relaxed);
2893    }
2894
2895    /// Get the memory budget
2896    pub fn budget(&self) -> &MemoryBudget {
2897        &self.budget
2898    }
2899
2900    /// Total local budget enforced jointly by this manager and any overlays.
2901    pub fn budget_limit_bytes(&self) -> u64 {
2902        self.budget.device_bytes
2903    }
2904
2905    /// Get the underlying CUDA device
2906    pub fn device(&self) -> &Arc<CudaDevice> {
2907        &self.device
2908    }
2909
2910    /// Validate a caller-requested manual accounting release.
2911    ///
2912    /// Tracked allocations release themselves through their private owner
2913    /// path. A public caller cannot authenticate ownership, so nonzero manual
2914    /// releases are refused while bytes are live and underflow is rejected
2915    /// without mutating either counter.
2916    pub fn record_free(&self, bytes: u64) -> Result<()> {
2917        let _mutation = self
2918            .accounting
2919            .mutation_lock
2920            .lock()
2921            .expect("GPU memory accounting poisoned");
2922        let admitted = self.accounting.allocated.load(Ordering::SeqCst);
2923        let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
2924        if admitted != 0 || reserved != 0 {
2925            return Err(XlogError::Kernel(format!(
2926                "manual GPU accounting release refused with live tracked bytes: admitted_bytes={} reserved_bytes={}",
2927                admitted, reserved
2928            )));
2929        }
2930        if bytes != 0 {
2931            return Err(XlogError::Kernel(format!(
2932                "manual GPU accounting release underflow: current_bytes=0 requested_bytes={}",
2933                bytes
2934            )));
2935        }
2936        Ok(())
2937    }
2938
2939    /// v0.6 device-runtime entry point: allocate `bytes` raw bytes
2940    /// through the attached [`XlogDeviceRuntime`].
2941    ///
2942    /// Returns a [`RuntimeAllocBlock`] that owns the allocation. On
2943    /// drop, the block deallocates through the runtime and updates
2944    /// both the manager's local `allocated` counter and the
2945    /// runtime's bookkeeping.
2946    ///
2947    /// Both budgets apply: the manager's local
2948    /// `MemoryBudget::device_bytes` AND any `GlobalDeviceBudget`
2949    /// stacked above the runtime's underlying resource. Either
2950    /// rejecting the request returns an `XlogError`. On runtime
2951    /// rejection the local reservation is rolled back so subsequent
2952    /// allocations see consistent state.
2953    ///
2954    /// # Errors
2955    /// * `XlogError::Kernel` if no runtime is attached.
2956    /// * `XlogError::ResourceExhausted` if the local budget cannot
2957    ///   accommodate the request.
2958    /// * `XlogError::ResourceExhausted` if the runtime's budget rejects the
2959    ///   request. Other runtime errors are reported as `XlogError::Kernel`.
2960    pub fn alloc_raw(self: &Arc<Self>, bytes: usize, tag: AllocTag) -> Result<RuntimeAllocBlock> {
2961        if self.runtime.is_none() {
2962            return Err(XlogError::Kernel(
2963                "GpuMemoryManager::alloc_raw called without an attached XlogDeviceRuntime; \
2964                 construct via with_runtime to enable runtime routing"
2965                    .to_string(),
2966            ));
2967        }
2968        let bytes_u64 = u64::try_from(bytes)
2969            .map_err(|_| XlogError::Kernel("Allocation size overflow".to_string()))?;
2970        self.reserve_local_bytes(bytes_u64, "manager_alloc_raw")?;
2971        let attempt = LocalAllocationAttempt::new(Arc::clone(&self.accounting), bytes_u64, None);
2972        self.alloc_raw_after_local_reservation(
2973            bytes,
2974            bytes_u64,
2975            tag,
2976            None,
2977            Arc::clone(&attempt.reclamation),
2978        )
2979        .map_err(|error| map_resource_error(error, self.peak_bytes()))
2980    }
2981
2982    fn alloc_raw_after_local_reservation(
2983        self: &Arc<Self>,
2984        bytes: usize,
2985        bytes_u64: u64,
2986        tag: AllocTag,
2987        runtime_reservation: Option<&mut RuntimeMemoryReservation>,
2988        reclamation: Arc<AllocationReclamation>,
2989    ) -> crate::device_runtime::ResourceResult<RuntimeAllocBlock> {
2990        let runtime = self.runtime.as_ref().ok_or_else(|| {
2991            ResourceError::Driver("raw allocation requires an attached device runtime".into())
2992        })?;
2993        #[cfg(test)]
2994        self.run_after_local_reservation_hook(bytes_u64);
2995        let request = AllocationRequest {
2996            bytes,
2997            stream: StreamId::DEFAULT,
2998            tag,
2999            reservation_pressure_bytes: 0,
3000            reclamation: Arc::clone(&reclamation),
3001        };
3002        let allocation = match runtime_reservation {
3003            Some(reservation) => reservation.materialize(request),
3004            None => runtime.materialize(request),
3005        };
3006        match allocation {
3007            Ok(block) => {
3008                // Arm the allocated block and backend before any fallible lookup.
3009                let owner = ResourceBlockRetirement::new(
3010                    block,
3011                    runtime.retirement_resource(),
3012                    Arc::clone(&reclamation),
3013                );
3014                let bind_charge: crate::device_runtime::ResourceResult<_> = (|| {
3015                    let block = owner.block();
3016                    let dependencies =
3017                        runtime
3018                            .allocation_dependencies(BlockId::from_block(block), block.bytes)?
3019                            .ok_or_else(|| {
3020                                ResourceError::Driver(
3021                            "runtime allocation does not provide physical release ownership".into())
3022                            })?;
3023                    if !Arc::ptr_eq(&dependencies.reclamation, &reclamation) {
3024                        return Err(ResourceError::Driver(
3025                            "runtime allocation replaced its accounting owner".into(),
3026                        ));
3027                    }
3028                    let allocation = dependencies.retain_allocation()?;
3029                    Ok((dependencies, allocation))
3030                })();
3031                let (dependencies, allocation) = match bind_charge {
3032                    Ok(owners) => owners,
3033                    Err(error) => {
3034                        return Err(error.retaining(bytes, reclamation));
3035                    }
3036                };
3037                let block = owner.into_block();
3038                Ok(RuntimeAllocBlock {
3039                    bytes: bytes_u64,
3040                    manager: Arc::clone(self),
3041                    runtime: Arc::clone(runtime),
3042                    block: Some(block),
3043                    dependencies,
3044                    allocation: Some(allocation),
3045                })
3046            }
3047            Err(error) => Err(error),
3048        }
3049    }
3050
3051    /// Get remaining budget in bytes
3052    pub fn remaining_bytes(&self) -> u64 {
3053        let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
3054        self.budget.device_bytes.saturating_sub(reserved)
3055    }
3056
3057    /// Reset diagnostic tracking only when no tracked bytes are live.
3058    pub fn reset_tracking(&self) -> Result<()> {
3059        let _mutation = self
3060            .accounting
3061            .mutation_lock
3062            .lock()
3063            .expect("GPU memory accounting poisoned");
3064        let admitted = self.accounting.allocated.load(Ordering::SeqCst);
3065        let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
3066        if admitted != 0 || reserved != 0 {
3067            return Err(XlogError::Kernel(format!(
3068                "GPU accounting reset refused with live tracked bytes: admitted_bytes={} reserved_bytes={}",
3069                admitted, reserved
3070            )));
3071        }
3072        self.accounting.peak.store(0, Ordering::SeqCst);
3073        Ok(())
3074    }
3075
3076    #[cfg(test)]
3077    fn run_after_local_reservation_hook(&self, bytes: u64) {
3078        let hook = self
3079            .after_local_reservation_hook
3080            .lock()
3081            .expect("after-local-reservation test hook poisoned")
3082            .clone();
3083        if let Some(hook) = hook {
3084            hook(bytes);
3085        }
3086    }
3087}
3088
3089fn map_resource_error(e: ResourceError, prior_peak_bytes: u64) -> XlogError {
3090    match e {
3091        error @ ResourceError::AllocationRetained(_) => XlogError::Kernel(error.to_string()),
3092        ResourceError::OutOfBudget {
3093            requested,
3094            current,
3095            limit,
3096            ..
3097        } => MemoryPressure {
3098            layer: "device_runtime",
3099            current_bytes: current as u128,
3100            requested_bytes: requested as u128,
3101            budget_bytes: u64::try_from(limit).unwrap_or(u64::MAX),
3102            prior_peak_bytes,
3103        }
3104        .into_error(),
3105        ResourceError::Driver(msg) => XlogError::Kernel(format!("device-runtime driver: {}", msg)),
3106        ResourceError::StreamMisuse(msg) => {
3107            XlogError::Kernel(format!("device-runtime stream misuse: {}", msg))
3108        }
3109        ResourceError::UseAfterFree { generation } => XlogError::Kernel(format!(
3110            "device-runtime use-after-free on generation {:?}",
3111            generation
3112        )),
3113        ResourceError::OutOfBounds { generation } => XlogError::Kernel(format!(
3114            "device-runtime out-of-bounds on generation {:?}",
3115            generation
3116        )),
3117    }
3118}
3119
3120/// Owned handle for a raw allocation routed through the device runtime.
3121/// Both raw allocations and typed storage views retain this handle. Drop hands
3122/// its block identity to the runtime; the actual raw owner's successful physical
3123/// reclamation returns the local manager charge, including on another reaper.
3124pub struct RuntimeAllocBlock {
3125    bytes: u64,
3126    manager: Arc<GpuMemoryManager>,
3127    runtime: Arc<XlogDeviceRuntime>,
3128    dependencies: Arc<DeviceAccessDependencies>,
3129    allocation: Option<Arc<RawDeviceAllocation>>,
3130    /// `None` after Drop fires; `Some(_)` while the block is live.
3131    /// Wrapped in Option so `Drop` can move the block out and pass
3132    /// it by value to `runtime.deallocate`.
3133    block: Option<DeviceBlock>,
3134}
3135
3136fn allocation_reclamation_outcome(
3137    physically_released: bool,
3138    queue_result: crate::device_runtime::ResourceResult<()>,
3139) -> crate::device_runtime::ResourceResult<()> {
3140    if physically_released {
3141        return Ok(());
3142    }
3143    queue_result?;
3144    Err(ResourceError::Driver(
3145        "allocation reclamation is still owned by another pending reaper".into(),
3146    ))
3147}
3148
3149impl RuntimeAllocBlock {
3150    fn release(&mut self) -> crate::device_runtime::ResourceResult<()> {
3151        // The backend already owns the actual raw allocation. Dropping these
3152        // bookkeeping references must not strand a runtime after a late reap.
3153        // Backend Drop itself transfers unproven allocations to cold retirement.
3154        let Some(block) = self.block.take() else {
3155            return allocation_reclamation_outcome(
3156                self.dependencies.allocation_was_released(),
3157                Ok(()),
3158            );
3159        };
3160        // The backend still owns the live allocation. Relinquish this wrapper's
3161        // lease before asking the reaper whether any other real owner remains.
3162        drop(self.allocation.take());
3163        let mut retirement = ResourceBlockRetirement::new(
3164            block,
3165            self.runtime.retirement_resource(),
3166            Arc::clone(&self.dependencies.reclamation),
3167        );
3168        let queue_result = retirement.release();
3169        match allocation_reclamation_outcome(
3170            self.dependencies.allocation_was_released(),
3171            queue_result,
3172        ) {
3173            Ok(()) => Ok(()),
3174            Err(error) => {
3175                self.manager.record_deallocation_failure(self.bytes);
3176                Err(error)
3177            }
3178        }
3179    }
3180
3181    /// Raw device pointer for this allocation. Live until the
3182    /// block is dropped.
3183    pub fn ptr(&self) -> u64 {
3184        self.block
3185            .as_ref()
3186            .expect("RuntimeAllocBlock used after drop")
3187            .ptr
3188    }
3189
3190    /// Allocation size in bytes.
3191    pub fn bytes(&self) -> usize {
3192        self.bytes as usize
3193    }
3194
3195    /// Borrow the underlying [`DeviceBlock`] metadata. Test/
3196    /// diagnostic accessor.
3197    pub fn device_block(&self) -> &DeviceBlock {
3198        self.block
3199            .as_ref()
3200            .expect("RuntimeAllocBlock used after drop")
3201    }
3202}
3203
3204impl std::fmt::Debug for RuntimeAllocBlock {
3205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3206        let mut dbg = f.debug_struct("RuntimeAllocBlock");
3207        dbg.field("bytes", &self.bytes);
3208        match &self.block {
3209            Some(b) => {
3210                dbg.field("ptr", &format_args!("{:#x}", b.ptr));
3211                dbg.field("device_ordinal", &b.device_ordinal);
3212                dbg.field("alloc_stream", &b.alloc_stream);
3213                dbg.field("tag", &b.tag);
3214                dbg.field("generation", &b.generation);
3215                dbg.field("state", &b.state);
3216            }
3217            None => {
3218                dbg.field("block", &"<dropped>");
3219            }
3220        }
3221        dbg.finish()
3222    }
3223}
3224
3225impl Drop for RuntimeAllocBlock {
3226    fn drop(&mut self) {
3227        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.release()));
3228    }
3229}
3230
3231/// Column data stored in device memory.
3232///
3233/// Most columns are owned by XLOG (`Owned`) and tracked against the memory budget. Columns may
3234/// also be imported via DLPack (`Dlpack`) or Arrow device (`ArrowDevice`) without copies; these are
3235/// freed via the DLPack deleter or Arrow release callback.
3236pub enum CudaColumn {
3237    Owned(TrackedCudaSlice<u8>),
3238    Dlpack(DlpackColumn),
3239    ArrowDevice(ArrowDeviceColumn),
3240}
3241
3242pub struct DlpackColumn {
3243    ptr: cudarc::driver::sys::CUdeviceptr,
3244    len_bytes: usize,
3245    stream: Arc<CudaStream>,
3246    storage: Arc<DeviceStorage>,
3247    /// `Some` when this DLPack column wraps memory that xlog
3248    /// itself owns through the device runtime — i.e. the
3249    /// caller exported an xlog-allocated slice via DLPack and
3250    /// kept ownership inside xlog. The strong reference keeps
3251    /// the source slice's [`crate::device_runtime::DeviceBlock`]
3252    /// reachable for runtime-block identity propagation, and
3253    /// keeps the underlying allocation alive across the
3254    /// DLPack handoff (drop order: column → tensor →
3255    /// `source_slice` → `runtime.deallocate`).
3256    ///
3257    /// `None` for true external DLPack producers; those
3258    /// columns continue to be rejected by strict-mode launch
3259    /// recorders.
3260    source_slice: Option<Arc<TrackedCudaSlice<u8>>>,
3261}
3262
3263pub struct ArrowDeviceColumn {
3264    ptr: cudarc::driver::sys::CUdeviceptr,
3265    len_bytes: usize,
3266    stream: Arc<CudaStream>,
3267    storage: Arc<DeviceStorage>,
3268    /// Same role as [`DlpackColumn::source_slice`]: `Some` for
3269    /// xlog-owned Arrow device columns, `None` for true
3270    /// external Arrow producers.
3271    source_slice: Option<Arc<TrackedCudaSlice<u8>>>,
3272}
3273
3274impl CudaColumn {
3275    pub fn owned(slice: TrackedCudaSlice<u8>) -> Self {
3276        Self::Owned(slice)
3277    }
3278
3279    /// Construct a column over an authenticated foreign allocation.
3280    ///
3281    /// # Safety
3282    /// The byte range must be live in `stream`'s CUDA context and retained by
3283    /// `tensor`. The producer must have completed its work before handoff and
3284    /// must coordinate subsequent non-XLOG accesses for the whole import lifetime.
3285    pub unsafe fn dlpack(
3286        ptr: cudarc::driver::sys::CUdeviceptr,
3287        len_bytes: usize,
3288        stream: Arc<CudaStream>,
3289        tensor: DlpackManagedTensor,
3290    ) -> Self {
3291        let storage = DeviceStorage::new(
3292            Backing::Foreign {
3293                _owner: Box::new(tensor),
3294                _source: None,
3295                bytes: len_bytes as u64,
3296            },
3297            Arc::clone(&stream),
3298            ptr,
3299        );
3300        Self::Dlpack(DlpackColumn {
3301            ptr,
3302            len_bytes,
3303            stream,
3304            storage,
3305            source_slice: None,
3306        })
3307    }
3308
3309    /// Construct a DLPack column that wraps memory **xlog
3310    /// itself owns** through the device runtime.
3311    ///
3312    /// Use this when xlog allocated `source_slice` via the
3313    /// runtime-backed manager and is exporting it as a DLPack
3314    /// tensor for inspection by external code while retaining
3315    /// ownership. The resulting column reports
3316    /// [`Self::is_external`] as `false` and
3317    /// [`Self::runtime_block`] returns the slice's
3318    /// [`crate::device_runtime::DeviceBlock`] — strict-mode
3319    /// launch recorders will record it normally instead of
3320    /// rejecting.
3321    ///
3322    /// True external DLPack producers (DLPack tensors handed
3323    /// to xlog by another framework) must continue to use
3324    /// [`Self::dlpack`].
3325    pub fn dlpack_xlog_owned(
3326        source_slice: Arc<TrackedCudaSlice<u8>>,
3327        stream: Arc<CudaStream>,
3328        tensor: DlpackManagedTensor,
3329    ) -> Self {
3330        let ptr = *source_slice.device_ptr();
3331        let len_bytes = source_slice.len();
3332        assert_eq!(
3333            source_slice.stream().context().cu_ctx(),
3334            stream.context().cu_ctx(),
3335            "import stream must belong to the source allocation context"
3336        );
3337        let storage = DeviceStorage::new(
3338            Backing::Foreign {
3339                _owner: Box::new(tensor),
3340                _source: Some(Arc::clone(&source_slice)),
3341                bytes: len_bytes as u64,
3342            },
3343            Arc::clone(&stream),
3344            ptr,
3345        );
3346        Self::Dlpack(DlpackColumn {
3347            ptr,
3348            len_bytes,
3349            stream,
3350            storage,
3351            source_slice: Some(source_slice),
3352        })
3353    }
3354
3355    /// Construct a column over an authenticated Arrow device allocation.
3356    ///
3357    /// # Safety
3358    /// `import` must retain this live byte range in `stream`'s CUDA context.
3359    /// Producer work must be complete and subsequent non-XLOG access coordinated
3360    /// for the entire lifetime of this imported storage.
3361    pub unsafe fn arrow_device(
3362        ptr: cudarc::driver::sys::CUdeviceptr,
3363        len_bytes: usize,
3364        stream: Arc<CudaStream>,
3365        import: Arc<ArrowDeviceImport>,
3366    ) -> Self {
3367        let storage = DeviceStorage::new(
3368            Backing::Foreign {
3369                _owner: Box::new(import),
3370                _source: None,
3371                bytes: len_bytes as u64,
3372            },
3373            Arc::clone(&stream),
3374            ptr,
3375        );
3376        Self::ArrowDevice(ArrowDeviceColumn {
3377            ptr,
3378            len_bytes,
3379            stream,
3380            storage,
3381            source_slice: None,
3382        })
3383    }
3384
3385    /// Construct an Arrow device column that wraps memory
3386    /// **xlog itself owns** through the device runtime. Same
3387    /// contract as [`Self::dlpack_xlog_owned`]: identity is
3388    /// preserved, strict recorders accept the column, and
3389    /// drop order keeps the underlying allocation alive
3390    /// through the Arrow handoff.
3391    ///
3392    /// True external Arrow device producers must continue to
3393    /// use [`Self::arrow_device`].
3394    pub fn arrow_device_xlog_owned(
3395        source_slice: Arc<TrackedCudaSlice<u8>>,
3396        stream: Arc<CudaStream>,
3397        import: Arc<ArrowDeviceImport>,
3398    ) -> Self {
3399        let ptr = *source_slice.device_ptr();
3400        let len_bytes = source_slice.len();
3401        assert_eq!(
3402            source_slice.stream().context().cu_ctx(),
3403            stream.context().cu_ctx(),
3404            "import stream must belong to the source allocation context"
3405        );
3406        let storage = DeviceStorage::new(
3407            Backing::Foreign {
3408                _owner: Box::new(import),
3409                _source: Some(Arc::clone(&source_slice)),
3410                bytes: len_bytes as u64,
3411            },
3412            Arc::clone(&stream),
3413            ptr,
3414        );
3415        Self::ArrowDevice(ArrowDeviceColumn {
3416            ptr,
3417            len_bytes,
3418            stream,
3419            storage,
3420            source_slice: Some(source_slice),
3421        })
3422    }
3423
3424    pub fn stream(&self) -> &Arc<CudaStream> {
3425        match self {
3426            CudaColumn::Owned(slice) => slice.stream(),
3427            CudaColumn::Dlpack(col) => &col.stream,
3428            CudaColumn::ArrowDevice(col) => &col.stream,
3429        }
3430    }
3431
3432    pub fn device_ptr(&self) -> &cudarc::driver::sys::CUdeviceptr {
3433        match self {
3434            CudaColumn::Owned(slice) => slice.device_ptr(),
3435            CudaColumn::Dlpack(col) => &col.ptr,
3436            CudaColumn::ArrowDevice(col) => &col.ptr,
3437        }
3438    }
3439
3440    /// Stable identity of the xlog memory manager that owns this column.
3441    ///
3442    /// True external DLPack and Arrow device columns return `None`; wrappers
3443    /// retaining an xlog-owned source slice preserve that slice's identity.
3444    pub fn memory_manager_ptr_value(&self) -> Option<usize> {
3445        match self {
3446            CudaColumn::Owned(slice) => Some(slice.memory_manager_ptr_value()),
3447            CudaColumn::Dlpack(col) => col
3448                .source_slice
3449                .as_ref()
3450                .map(|slice| slice.memory_manager_ptr_value()),
3451            CudaColumn::ArrowDevice(col) => col
3452                .source_slice
3453                .as_ref()
3454                .map(|slice| slice.memory_manager_ptr_value()),
3455        }
3456    }
3457
3458    pub(crate) fn runtime_allocation_identity(&self) -> Result<Option<RuntimeAllocationIdentity>> {
3459        match self {
3460            CudaColumn::Owned(slice) => slice.runtime_allocation_identity(),
3461            CudaColumn::Dlpack(col) => {
3462                let Some(source) = &col.source_slice else {
3463                    return Ok(None);
3464                };
3465                let mut identity = source.runtime_allocation_identity()?;
3466                if let Some(identity) = &mut identity {
3467                    identity.allocation_ptr = col.ptr;
3468                    identity.allocation_bytes = col.len_bytes;
3469                }
3470                Ok(identity)
3471            }
3472            CudaColumn::ArrowDevice(col) => {
3473                let Some(source) = &col.source_slice else {
3474                    return Ok(None);
3475                };
3476                let mut identity = source.runtime_allocation_identity()?;
3477                if let Some(identity) = &mut identity {
3478                    identity.allocation_ptr = col.ptr;
3479                    identity.allocation_bytes = col.len_bytes;
3480                }
3481                Ok(identity)
3482            }
3483        }
3484    }
3485
3486    /// Borrow the underlying [`crate::device_runtime::DeviceBlock`].
3487    ///
3488    /// Returns `Some(&block)` when xlog owns the memory through
3489    /// the runtime — `Owned` slices that were allocated via a
3490    /// runtime-backed manager, AND `Dlpack` / `ArrowDevice`
3491    /// columns constructed via the `*_xlog_owned` constructors
3492    /// (where the source slice's block is reachable through
3493    /// the retained `Arc<TrackedCudaSlice<u8>>`).
3494    ///
3495    /// Returns `None` for legacy cudarc-backed `Owned` slices
3496    /// (no runtime block exists) and for true external
3497    /// `Dlpack` / `ArrowDevice` columns (xlog never owned the
3498    /// allocation). Strict-mode launch recorders reject `None`
3499    /// returns; permissive recorders silently skip.
3500    pub fn runtime_block(&self) -> Option<&crate::device_runtime::DeviceBlock> {
3501        match self {
3502            CudaColumn::Owned(slice) => slice.runtime_block(),
3503            CudaColumn::Dlpack(col) => col.source_slice.as_ref().and_then(|s| s.runtime_block()),
3504            CudaColumn::ArrowDevice(col) => {
3505                col.source_slice.as_ref().and_then(|s| s.runtime_block())
3506            }
3507        }
3508    }
3509
3510    /// Whether this column wraps externally-managed device
3511    /// memory.
3512    ///
3513    /// Returns `true` only for `Dlpack` / `ArrowDevice` columns
3514    /// where xlog never owned the allocation (no `source_slice`).
3515    /// `Dlpack` / `ArrowDevice` columns built via
3516    /// `*_xlog_owned` constructors return `false` — xlog still
3517    /// owns the memory; the DLPack / Arrow handle is just an
3518    /// export wrapper.
3519    ///
3520    /// External memory has no xlog-side runtime identity;
3521    /// strict launch recorders reject such columns and require
3522    /// callers to coordinate cross-stream synchronization
3523    /// themselves.
3524    pub fn is_external(&self) -> bool {
3525        match self {
3526            CudaColumn::Owned(_) => false,
3527            CudaColumn::Dlpack(col) => col.source_slice.is_none(),
3528            CudaColumn::ArrowDevice(col) => col.source_slice.is_none(),
3529        }
3530    }
3531}
3532
3533impl From<TrackedCudaSlice<u8>> for CudaColumn {
3534    fn from(value: TrackedCudaSlice<u8>) -> Self {
3535        CudaColumn::Owned(value)
3536    }
3537}
3538
3539impl DeviceSlice<u8> for CudaColumn {
3540    fn len(&self) -> usize {
3541        match self {
3542            CudaColumn::Owned(slice) => slice.len(),
3543            CudaColumn::Dlpack(col) => col.len_bytes,
3544            CudaColumn::ArrowDevice(col) => col.len_bytes,
3545        }
3546    }
3547
3548    fn stream(&self) -> &Arc<CudaStream> {
3549        self.stream()
3550    }
3551}
3552
3553impl private_access::Sealed for CudaColumn {}
3554
3555impl DeviceRead<u8> for CudaColumn {
3556    fn device_view(&self) -> DeviceMemoryView<u8> {
3557        match self {
3558            CudaColumn::Owned(slice) => slice.view(),
3559            CudaColumn::Dlpack(col) => DeviceMemoryView {
3560                ptr: col.ptr,
3561                len: col.len_bytes,
3562                storage: Arc::clone(&col.storage),
3563                element: std::marker::PhantomData,
3564            },
3565            CudaColumn::ArrowDevice(col) => DeviceMemoryView {
3566                ptr: col.ptr,
3567                len: col.len_bytes,
3568                storage: Arc::clone(&col.storage),
3569                element: std::marker::PhantomData,
3570            },
3571        }
3572    }
3573}
3574
3575impl DeviceWrite<u8> for CudaColumn {}
3576
3577impl AsKernelParam for &CudaColumn {
3578    fn as_kernel_param(&self) -> *mut std::ffi::c_void {
3579        ((self.device_ptr()) as *const cudarc::driver::sys::CUdeviceptr)
3580            .cast_mut()
3581            .cast()
3582    }
3583}
3584
3585impl AsKernelParam for &mut CudaColumn {
3586    fn as_kernel_param(&self) -> *mut std::ffi::c_void {
3587        ((self.device_ptr()) as *const cudarc::driver::sys::CUdeviceptr)
3588            .cast_mut()
3589            .cast()
3590    }
3591}
3592
3593impl<'a> IntoKernelParamStorage for &'a CudaColumn {
3594    type Storage = DeviceParamStorage<'a>;
3595
3596    fn into_kernel_param_storage(self) -> Self::Storage {
3597        match self {
3598            CudaColumn::Owned(slice) => slice.into_kernel_param_storage(),
3599            CudaColumn::Dlpack(col) => DeviceParamStorage::unsynced(col.ptr),
3600            CudaColumn::ArrowDevice(col) => DeviceParamStorage::unsynced(col.ptr),
3601        }
3602    }
3603}
3604
3605impl<'a> IntoKernelParamStorage for &'a mut CudaColumn {
3606    type Storage = DeviceParamStorage<'a>;
3607
3608    fn into_kernel_param_storage(self) -> Self::Storage {
3609        match self {
3610            CudaColumn::Owned(slice) => slice.into_kernel_param_storage(),
3611            CudaColumn::Dlpack(col) => DeviceParamStorage::unsynced(col.ptr),
3612            CudaColumn::ArrowDevice(col) => DeviceParamStorage::unsynced(col.ptr),
3613        }
3614    }
3615}
3616
3617/// Column-oriented GPU buffer
3618///
3619/// Holds columnar data on the GPU with an associated schema.
3620/// Each column is stored as a separate `CudaSlice<u8>`.
3621pub struct CudaBuffer {
3622    /// Column data stored as raw bytes
3623    pub(crate) columns: Vec<CudaColumn>,
3624    /// Row capacity for allocated columns
3625    pub(crate) row_cap: u64,
3626    /// Device-resident row count (len = 1)
3627    pub(crate) d_num_rows: TrackedCudaSlice<u32>,
3628    /// Schema describing the column types
3629    pub(crate) schema: Schema,
3630    /// Cached host-side row count (u32::MAX = not yet cached).
3631    /// Avoids repeated synchronous D2H transfers between explicit mutations,
3632    /// whose public accessors invalidate this cache before returning.
3633    cached_row_count: AtomicU32,
3634    /// True only when construction or a set operation proves that rows are
3635    /// lexicographically sorted by every schema column and full-row unique.
3636    canonical_full_row_set_certified: bool,
3637}
3638
3639impl CudaBuffer {
3640    /// Create a buffer from existing columns
3641    ///
3642    /// # Arguments
3643    /// * `columns` - Pre-allocated column data
3644    /// * `row_cap` - Row capacity for the buffer
3645    /// * `d_num_rows` - Device-resident row count
3646    /// * `schema` - Schema describing the columns
3647    ///
3648    /// # Panics
3649    /// Panics if the number of columns doesn't match the schema arity
3650    pub fn from_columns(
3651        columns: Vec<CudaColumn>,
3652        row_cap: u64,
3653        d_num_rows: TrackedCudaSlice<u32>,
3654        schema: Schema,
3655    ) -> Self {
3656        assert_eq!(
3657            columns.len(),
3658            schema.arity(),
3659            "Number of columns ({}) must match schema arity ({})",
3660            columns.len(),
3661            schema.arity()
3662        );
3663        Self {
3664            columns,
3665            row_cap,
3666            d_num_rows,
3667            schema,
3668            cached_row_count: AtomicU32::new(u32::MAX),
3669            canonical_full_row_set_certified: false,
3670        }
3671    }
3672
3673    /// Like `from_columns`, but eagerly populates the row-count cache.
3674    /// Use when the host already knows the exact row count (e.g., `buffer_from_columns`).
3675    pub fn from_columns_with_host_count(
3676        columns: Vec<CudaColumn>,
3677        row_cap: u64,
3678        d_num_rows: TrackedCudaSlice<u32>,
3679        schema: Schema,
3680        host_row_count: u32,
3681    ) -> Self {
3682        assert_eq!(
3683            columns.len(),
3684            schema.arity(),
3685            "Number of columns ({}) must match schema arity ({})",
3686            columns.len(),
3687            schema.arity()
3688        );
3689        Self {
3690            columns,
3691            row_cap,
3692            d_num_rows,
3693            schema,
3694            cached_row_count: AtomicU32::new(host_row_count),
3695            canonical_full_row_set_certified: false,
3696        }
3697    }
3698
3699    /// Returns the cached row count if available (not sentinel `u32::MAX`).
3700    pub fn cached_row_count(&self) -> Option<u32> {
3701        let v = self.cached_row_count.load(Ordering::Relaxed);
3702        if v == u32::MAX {
3703            None
3704        } else {
3705            Some(v)
3706        }
3707    }
3708
3709    /// Sets the cached row count if not already set (CAS from sentinel).
3710    /// No-op if already cached.
3711    pub(crate) fn set_cached_row_count_if_unset(&self, count: u32) {
3712        let _ = self.cached_row_count.compare_exchange(
3713            u32::MAX,
3714            count,
3715            Ordering::Relaxed,
3716            Ordering::Relaxed,
3717        );
3718    }
3719
3720    pub(crate) fn set_cached_row_count_after_device_copy(&self, count: u32) {
3721        self.cached_row_count.store(count, Ordering::Relaxed);
3722    }
3723
3724    pub(crate) fn invalidate_cached_row_count(&self) {
3725        self.cached_row_count.store(u32::MAX, Ordering::Relaxed);
3726    }
3727
3728    /// Whether rows are sorted in schema-column order and full-row unique.
3729    pub fn canonical_full_row_set_certified(&self) -> bool {
3730        self.canonical_full_row_set_certified
3731    }
3732
3733    /// Record a full-schema ordering and uniqueness proof from a set operation.
3734    pub(crate) fn certify_canonical_full_row_set(&mut self) {
3735        self.canonical_full_row_set_certified = true;
3736    }
3737
3738    /// Borrow every column without permitting mutation of certified contents.
3739    pub fn columns(&self) -> &[CudaColumn] {
3740        &self.columns
3741    }
3742
3743    /// Mutably borrow columns after invalidating canonical-set metadata.
3744    pub fn columns_mut(&mut self) -> &mut [CudaColumn] {
3745        self.canonical_full_row_set_certified = false;
3746        &mut self.columns
3747    }
3748
3749    /// Replace the schema after invalidating canonical ordering metadata.
3750    pub fn set_schema(&mut self, schema: Schema) {
3751        assert_eq!(self.columns.len(), schema.arity());
3752        self.schema = schema;
3753        self.canonical_full_row_set_certified = false;
3754    }
3755
3756    /// Set row capacity and invalidate all host-derived row metadata.
3757    pub fn set_row_capacity(&mut self, row_cap: u64) {
3758        self.row_cap = row_cap;
3759        self.cached_row_count.store(u32::MAX, Ordering::Relaxed);
3760        self.canonical_full_row_set_certified = false;
3761    }
3762
3763    /// Mutably borrow the device row count after invalidating derived metadata.
3764    pub fn num_rows_device_mut(&mut self) -> &mut TrackedCudaSlice<u32> {
3765        self.cached_row_count.store(u32::MAX, Ordering::Relaxed);
3766        self.canonical_full_row_set_certified = false;
3767        &mut self.d_num_rows
3768    }
3769
3770    /// Get the row capacity
3771    pub fn num_rows(&self) -> u64 {
3772        self.row_cap
3773    }
3774
3775    /// Get the device-resident row count
3776    pub fn num_rows_device(&self) -> &TrackedCudaSlice<u32> {
3777        &self.d_num_rows
3778    }
3779
3780    /// Check if the buffer has zero row capacity
3781    pub fn is_empty(&self) -> bool {
3782        self.row_cap == 0
3783    }
3784
3785    /// Get the schema
3786    pub fn schema(&self) -> &Schema {
3787        &self.schema
3788    }
3789
3790    /// Get the number of columns (arity)
3791    pub fn arity(&self) -> usize {
3792        self.schema.arity()
3793    }
3794
3795    /// Estimated memory usage in bytes
3796    pub fn estimated_bytes(&self) -> u64 {
3797        self.row_cap * self.schema.row_size_bytes() as u64
3798    }
3799
3800    /// Get a reference to a specific column by index
3801    pub fn column(&self, index: usize) -> Option<&CudaColumn> {
3802        self.columns.get(index)
3803    }
3804}
3805
3806pub fn validate_logical_row_count(row_cap: u64, logical_rows: usize) -> Result<usize> {
3807    let row_cap_usize = usize::try_from(row_cap)
3808        .map_err(|_| XlogError::Kernel(format!("Row capacity {} exceeds usize::MAX", row_cap)))?;
3809    if logical_rows > row_cap_usize {
3810        return Err(XlogError::Kernel(format!(
3811            "Logical row count {} exceeds row capacity {}",
3812            logical_rows, row_cap
3813        )));
3814    }
3815    debug_assert!(logical_rows <= row_cap_usize);
3816    Ok(logical_rows)
3817}
3818
3819#[cfg(test)]
3820pub(crate) fn test_memory_manifest(
3821    on_drop: impl FnOnce() + Send + Sync + 'static,
3822) -> (Arc<MemoryAccessManifest>, std::sync::Weak<[u8]>) {
3823    struct Bytes<F: FnOnce()>(Arc<[u8]>, Option<F>);
3824    impl<F: FnOnce()> Drop for Bytes<F> {
3825        fn drop(&mut self) {
3826            if let Some(on_drop) = self.1.take() {
3827                on_drop();
3828            }
3829        }
3830    }
3831    impl<F: FnOnce() + Send + Sync + 'static> MemoryStorageOwner for Bytes<F> {
3832        fn dependencies(
3833            &self,
3834        ) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
3835            panic!("a passive host manifest must not touch CUDA dependencies")
3836        }
3837    }
3838    let bytes = Arc::<[u8]>::from(vec![7; 64]);
3839    let weak = Arc::downgrade(&bytes);
3840    let owner = Arc::new(Bytes(bytes, Some(on_drop)));
3841    let mut manifest = MemoryAccessManifest::default();
3842    manifest.ranges.push((
3843        17,
3844        MemoryUse::new(owner.0.as_ptr() as u64, owner.0.len(), Access::ReadWrite).unwrap(),
3845    ));
3846    manifest.retained.push(RetainedStorageUse {
3847        owner,
3848        access: Access::ReadWrite,
3849    });
3850    (Arc::new(manifest), weak)
3851}
3852
3853#[cfg(test)]
3854mod tests {
3855    impl crate::launch::RecorderCleanup<u8> for OperationCompletion {
3856        fn synchronize_retired(&self) -> ResourceResult<()> {
3857            if self.is_complete() {
3858                Ok(())
3859            } else {
3860                Err(ResourceError::Driver("completion remains unknown".into()))
3861            }
3862        }
3863
3864        fn cancel_retired(&self, _use: u8) -> ResourceResult<()> {
3865            Ok(())
3866        }
3867    }
3868
3869    #[test]
3870    fn retired_context_completion_retries_unknown_wait_without_changing_execution_identity() {
3871        let proof = OperationCompletion::new(17);
3872        assert!(proof
3873            .synchronize_retired_with(|| Err(ResourceError::Driver("context wait failed".into())))
3874            .is_err());
3875        assert!(!proof.is_complete());
3876        assert_eq!(proof.execution_id, 17);
3877        assert!(std::panic::catch_unwind(|| {
3878            let _ = proof.synchronize_retired_with(|| panic!("context wait panic"));
3879        })
3880        .is_err());
3881        assert!(!proof.is_complete());
3882        proof.synchronize_retired_with(|| Ok(())).unwrap();
3883        proof
3884            .synchronize_retired_with(|| panic!("completed wait must not repeat"))
3885            .unwrap();
3886        assert!(proof.is_complete());
3887        assert_eq!(proof.execution_id, 17);
3888        assert!(proof.validate_submission(17).is_err());
3889        assert!(proof.validate_submission(18).is_err());
3890    }
3891
3892    #[test]
3893    #[ignore = "requires authorized CUDA execution"]
3894    fn failed_abort_reclaims_real_allocation_after_origin_thread_exit() {
3895        use crate::cuda_graph::{reap_capture_retirements, CapturedCudaGraph};
3896        use crate::device_runtime::{AsyncCudaResource, GlobalDeviceBudget, StreamPool};
3897        use std::sync::mpsc;
3898
3899        let _serial = crate::cuda_graph::capture_lifecycle_test_guard();
3900        let device = Arc::new(CudaDevice::new(0).unwrap());
3901        let default_stream = Arc::clone(device.inner().stream());
3902        let context = Arc::clone(device.inner().stream().context());
3903        assert!(
3904            context.has_async_alloc(),
3905            "this lifecycle regression requires real stream-ordered allocation and free"
3906        );
3907        let capture_stream = context.new_stream().unwrap();
3908        let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
3909        let resource = Box::new(GlobalDeviceBudget::new(
3910            Box::new(AsyncCudaResource::new(
3911                Arc::clone(&device),
3912                0,
3913                Arc::clone(&pool),
3914            )),
3915            4096,
3916        ));
3917        let runtime = Arc::new(XlogDeviceRuntime::with_resource(
3918            Arc::clone(&device),
3919            0,
3920            pool,
3921            resource,
3922        ));
3923        let manager = Arc::new(GpuMemoryManager::with_runtime(
3924            device,
3925            MemoryBudget::with_limit(4096),
3926            Arc::clone(&runtime),
3927        ));
3928        let (sent, received) = mpsc::channel();
3929        let (abort, wait_for_abort) = mpsc::channel();
3930        let worker_manager = Arc::clone(&manager);
3931        let worker = std::thread::spawn(move || {
3932            // Real caller-prefix work precedes the target allocation and
3933            // completed publication; keep its bytes for later observation.
3934            let stream = Arc::clone(worker_manager.device.inner().stream());
3935            let prefix = DeviceMemoryView::<u8>::allocate(
3936                Arc::clone(&stream),
3937                Arc::clone(worker_manager.device.inner().allocation_stream()),
3938                16,
3939            )
3940            .unwrap();
3941            with_memory_access(
3942                Arc::clone(&stream),
3943                vec![prefix.access(Access::Write).unwrap()],
3944                |_| {
3945                    // SAFETY: the admitted prefix owns these exact bytes.
3946                    unsafe {
3947                        cudarc::driver::sys::cuMemsetD8Async(
3948                            *prefix.device_ptr(),
3949                            0x3c,
3950                            16,
3951                            stream.cu_stream(),
3952                        )
3953                        .result()?;
3954                    }
3955                    Ok(())
3956                },
3957            )
3958            .unwrap();
3959            let allocation = worker_manager.alloc::<u8>(4096).unwrap();
3960            let raw = allocation.storage.dependencies.retain_allocation().unwrap();
3961            let stream = Arc::clone(allocation.stream());
3962            let ptr = *allocation.device_ptr();
3963            let submission = crate::cuda_graph::acquire_stream_submission_phase(&stream).unwrap();
3964            let execution_id = submission.execution_id();
3965            let mut operation = MemoryOperation {
3966                submission,
3967                transaction: admit_memory_access(
3968                    Arc::clone(&stream),
3969                    vec![allocation.view().access(Access::Write).unwrap()],
3970                    None,
3971                    &[],
3972                    execution_id,
3973                )
3974                .unwrap(),
3975            };
3976            let admitted = Arc::clone(operation.transaction.prepared_owner().unwrap());
3977            let completion = Arc::clone(admitted.completion());
3978            let owner = Arc::downgrade(&admitted);
3979            operation
3980                .transaction
3981                .enqueue_operation_with(
3982                    || {
3983                        let pin = operation.submission.pin()?;
3984                        admitted.bind_submission(&pin)?;
3985                        Ok(pin)
3986                    },
3987                    MemoryOperationOwner::prepare,
3988                    |pin| {
3989                        pin.with_serialized_submission(
3990                            || -> crate::device_runtime::ResourceResult<()> {
3991                                // SAFETY: this admitted operation owns the whole allocation;
3992                                // its real initialization dependencies were just prepared.
3993                                unsafe {
3994                                    cudarc::driver::sys::cuMemsetD8Async(
3995                                        ptr,
3996                                        0x5a,
3997                                        4096,
3998                                        stream.cu_stream(),
3999                                    )
4000                                    .result()?;
4001                                }
4002                                Ok(())
4003                            },
4004                        )
4005                    },
4006                    MemoryOperationOwner::synchronize,
4007                    MemoryOperationOwner::cancel,
4008                )
4009                .unwrap();
4010            drop(admitted);
4011            drop(allocation);
4012            sent.send((raw, completion, owner, prefix)).unwrap();
4013            wait_for_abort.recv().unwrap();
4014            // Inject only the failed original wait, not allocation, preparation,
4015            // enqueue, context recovery or physical free. Active real capture
4016            // defers cold recovery until this originating host thread has exited.
4017            let error = operation
4018                .transaction
4019                .abort_with(
4020                    |_| {
4021                        Err(ResourceError::Driver(
4022                            "injected original completion wait failure".into(),
4023                        ))
4024                    },
4025                    MemoryOperationOwner::cancel,
4026                )
4027                .unwrap_err();
4028            assert!(error
4029                .to_string()
4030                .contains("original completion wait failure"));
4031        });
4032        let (raw, completion, owner, prefix) = received.recv().unwrap();
4033        assert_ne!(
4034            completion.execution_id,
4035            crate::cuda_graph::stream_execution_id(&default_stream).unwrap(),
4036            "the reaper must not identify its own PTDS as the exited producer's execution"
4037        );
4038        let reclamation = Arc::clone(raw.reclamation());
4039        let graph = CapturedCudaGraph::capture_on_stream(&capture_stream, || {
4040            abort.send(()).unwrap();
4041            worker.join().unwrap();
4042            reap_capture_retirements();
4043            assert!(!completion.is_complete());
4044            assert!(owner.upgrade().is_some());
4045            assert!(!reclamation.was_released());
4046            assert_eq!(manager.allocated_bytes(), 4096);
4047            assert_eq!(runtime.bytes_outstanding(), 4096);
4048            assert!(matches!(
4049                runtime.reserve_memory(1),
4050                Err(ResourceError::OutOfBudget {
4051                    current: 4096,
4052                    remaining: 0,
4053                    ..
4054                })
4055            ));
4056            Ok(())
4057        })
4058        .unwrap();
4059        drop(graph);
4060        runtime.reap_pending().unwrap();
4061        assert!(completion.is_complete());
4062        assert!(owner.upgrade().is_none());
4063        // The separately retained real allocation prevents physical free even
4064        // after successful operation recovery and logical runtime detachment.
4065        let pending = Arc::clone(
4066            &reclamation
4067                .state
4068                .lock()
4069                .unwrap()
4070                .pending
4071                .as_ref()
4072                .unwrap()
4073                .0,
4074        );
4075        assert_eq!(pending.load(Ordering::Acquire), 4096);
4076        assert!(!reclamation.was_released());
4077        assert_eq!(manager.allocated_bytes(), 4096);
4078        assert_eq!(runtime.bytes_outstanding(), 4096);
4079        assert!(matches!(
4080            runtime.reserve_memory(1),
4081            Err(ResourceError::OutOfBudget {
4082                current: 4096,
4083                remaining: 0,
4084                ..
4085            })
4086        ));
4087        context.bind_to_thread().unwrap();
4088        let mut observed = [0u8; 4096];
4089        // SAFETY: successful context recovery completed the admitted write;
4090        // raw still owns all bytes and no other operation uses this allocation.
4091        unsafe {
4092            cudarc::driver::sys::cuMemcpyDtoH_v2(
4093                observed.as_mut_ptr().cast(),
4094                raw.ptr(),
4095                observed.len(),
4096            )
4097            .result()
4098            .unwrap();
4099        }
4100        assert!(observed.iter().all(|byte| *byte == 0x5a));
4101        let mut observed_prefix = [0u8; 16];
4102        // SAFETY: the same successful context barrier covers the actual caller
4103        // prefix, and its original allocation is still owned by prefix.
4104        unsafe {
4105            cudarc::driver::sys::cuMemcpyDtoH_v2(
4106                observed_prefix.as_mut_ptr().cast(),
4107                *prefix.device_ptr(),
4108                observed_prefix.len(),
4109            )
4110            .result()
4111            .unwrap();
4112        }
4113        assert_eq!(observed_prefix, [0x3c; 16]);
4114        drop(prefix);
4115        drop(raw);
4116        runtime.reap_pending().unwrap();
4117        assert!(reclamation.was_released());
4118        assert_eq!(pending.load(Ordering::Acquire), 0);
4119        assert_eq!(manager.allocated_bytes(), 0);
4120        assert_eq!(manager.remaining_bytes(), 4096);
4121        assert_eq!(runtime.bytes_outstanding(), 0);
4122        drop(runtime.reserve_memory(4096).unwrap());
4123    }
4124
4125    #[test]
4126    fn operation_completion_rejects_moved_execution_before_dependency_prepare() {
4127        let proof = Arc::new(OperationCompletion::new(17));
4128        let mut transaction =
4129            RecorderTransaction::from_admitted(Arc::clone(&proof), Box::new([0u8]));
4130        let mut cancelled = false;
4131        let result = transaction.enqueue_operation_with(
4132            || proof.validate_submission(18),
4133            |_| panic!("moved execution must not prepare dependencies"),
4134            |_| -> std::result::Result<(), &str> { panic!("moved execution must not launch") },
4135            |_| panic!("refused admission must not wait on another execution"),
4136            |_, _| {
4137                cancelled = true;
4138                Ok(())
4139            },
4140        );
4141        assert!(matches!(
4142            result,
4143            Err(crate::launch::LaunchEnqueueError::Preparation(_))
4144        ));
4145        assert!(cancelled);
4146        assert!(!proof.is_complete());
4147    }
4148
4149    #[test]
4150    fn operation_completion_requires_exact_successful_synchronization() {
4151        let proof = OperationCompletion::new(17);
4152        assert!(proof.validate_submission(18).is_err());
4153        assert!(proof
4154            .synchronize_with(18, || panic!("wrong stream must not be waited"))
4155            .is_err());
4156        assert!(!proof.is_complete());
4157        assert!(proof
4158            .synchronize_with(17, || Err(ResourceError::Driver("wait failed".into())))
4159            .is_err());
4160        assert!(!proof.is_complete());
4161        let unwound = std::panic::catch_unwind(|| {
4162            let _ = proof.synchronize_with(17, || panic!("wait unwound"));
4163        });
4164        assert!(unwound.is_err());
4165        assert!(!proof.is_complete());
4166        proof.synchronize_with(17, || Ok(())).unwrap();
4167        assert!(proof.is_complete());
4168        assert!(proof.validate_submission(17).is_err());
4169    }
4170
4171    #[test]
4172    fn operation_completion_survives_failed_reservation_cancellation() {
4173        let proof = Arc::new(OperationCompletion::new(17));
4174        let mut transaction =
4175            RecorderTransaction::from_admitted(Arc::clone(&proof), Box::new([0u8]));
4176        let result = transaction.enqueue_operation_with(
4177            || Ok(()),
4178            |proof| proof.validate_submission(17),
4179            |_| Err("record failed"),
4180            |proof| proof.synchronize_with(17, || Ok(())),
4181            |_, _| Err(ResourceError::Driver("cancellation failed".into())),
4182        );
4183        assert!(matches!(
4184            result,
4185            Err(crate::launch::LaunchEnqueueError::OperationAndCleanup { .. })
4186        ));
4187        assert!(proof.is_complete());
4188    }
4189
4190    #[test]
4191    fn passive_manifest_keeps_storage_and_context_qualified_coverage() {
4192        struct Bytes(Box<[u8]>);
4193        impl MemoryStorageOwner for Bytes {
4194            fn dependencies(
4195                &self,
4196            ) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
4197                panic!("a passive manifest must not touch CUDA dependencies")
4198            }
4199        }
4200        let bytes = Arc::new(Bytes(vec![7; 64].into_boxed_slice()));
4201        assert_eq!(bytes.0[0], 7);
4202        let weak = Arc::downgrade(&bytes);
4203        let mut source = MemoryAccessManifest::default();
4204        source
4205            .ranges
4206            .push((17, MemoryUse::new(0x1000, 64, Access::ReadWrite).unwrap()));
4207        source.retained.push(RetainedStorageUse {
4208            owner: bytes,
4209            access: Access::ReadWrite,
4210        });
4211        let source = Arc::new(source);
4212        let combined = MemoryAccessManifest::combine(std::slice::from_ref(&source));
4213        drop(source);
4214        assert!(weak.upgrade().is_some());
4215        assert!(combined.covers(&[(17, MemoryUse::new(0x1010, 16, Access::Read).unwrap())]));
4216        assert!(!combined.covers(&[(18, MemoryUse::new(0x1010, 16, Access::Read).unwrap())]));
4217        assert!(!combined.covers(&[(17, MemoryUse::new(0x1030, 32, Access::Read).unwrap())]));
4218        drop(combined);
4219        assert!(weak.upgrade().is_none());
4220    }
4221
4222    #[test]
4223    fn shared_reclamation_preserves_leases_until_the_actual_owner_can_retire() {
4224        use crate::device_runtime::resource::{BlockUseRegistry, MemoryUse};
4225        use std::sync::atomic::AtomicUsize;
4226
4227        struct PhysicalStorage {
4228            bytes: Box<[u8]>,
4229            drops: Arc<AtomicUsize>,
4230        }
4231        impl Drop for PhysicalStorage {
4232            fn drop(&mut self) {
4233                self.drops.fetch_add(1, Ordering::SeqCst);
4234            }
4235        }
4236        struct Payload {
4237            physical: Option<PhysicalStorage>,
4238            reclamation: Arc<AllocationReclamation>,
4239            admission: Option<crate::device_runtime::resource::MemoryUseGroup>,
4240        }
4241
4242        let registry = Arc::new(std::sync::Mutex::new(BlockUseRegistry::default()));
4243        let accounting = Arc::new(GpuMemoryAccounting::default());
4244        accounting.budget_reserved.store(64, Ordering::SeqCst);
4245        let reclamation = Arc::new(AllocationReclamation::default());
4246        reclamation
4247            .attach_local(Arc::clone(&accounting), 64)
4248            .unwrap();
4249        reclamation.acquired().unwrap();
4250        let physical_drops = Arc::new(AtomicUsize::new(0));
4251        let physical = PhysicalStorage {
4252            bytes: vec![3u8; 64].into_boxed_slice(),
4253            drops: Arc::clone(&physical_drops),
4254        };
4255        let memory = MemoryUse::new(physical.bytes.as_ptr() as u64, 64, Access::ReadWrite).unwrap();
4256        let mut pending = Some(Arc::new(Payload {
4257            physical: Some(physical),
4258            reclamation: Arc::clone(&reclamation),
4259            admission: None,
4260        }));
4261        let lease = Arc::clone(pending.as_ref().unwrap());
4262        let observer = Arc::downgrade(&lease);
4263
4264        // Logical detach leaves the actual bytes available to existing leases.
4265        // It must not reserve a physical free or invoke the driver boundary.
4266        assert!(!reclaim_shared_allocation(&mut pending, |_| {
4267            panic!("a live storage lease must prevent physical reclamation")
4268        })
4269        .unwrap());
4270        assert!(Arc::ptr_eq(pending.as_ref().unwrap(), &lease));
4271        let use_group = registry
4272            .lock()
4273            .unwrap()
4274            .reserve_owned_memory_uses(&[(7, memory)], &[reclamation.release_proof()])
4275            .unwrap();
4276        assert_eq!(lease.physical.as_ref().unwrap().bytes[9], 3);
4277        registry
4278            .lock()
4279            .unwrap()
4280            .release_memory_uses(use_group)
4281            .unwrap();
4282        drop(lease);
4283
4284        // The sole real owner now enters physical reclamation. Failed and
4285        // unwinding driver waits retain both its allocation and its admission.
4286        let attempt = |owner: &mut Payload| {
4287            crate::device_runtime::resource::with_reclamation_admission(
4288                &registry,
4289                7,
4290                memory,
4291                owner.reclamation.release_proof(),
4292                &mut owner.admission,
4293                || Err(ResourceError::Driver("physical completion pending".into())),
4294            )
4295        };
4296        assert!(reclaim_shared_allocation(&mut pending, attempt).is_err());
4297        assert!(observer.upgrade().is_none());
4298        let queue = std::sync::Mutex::new(vec![pending.take()]);
4299        assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4300            let mut batch =
4301                PendingReclamationBatch::take(&queue, |queue, owners| queue.append(owners));
4302            let _ = reclaim_shared_allocation(&mut batch[0], |_| panic!("wait unwound"));
4303        }))
4304        .is_err());
4305        pending = queue
4306            .lock()
4307            .unwrap()
4308            .pop()
4309            .expect("failed owner returned to canonical queue");
4310        assert!(pending.as_ref().unwrap().physical.is_some());
4311        assert_eq!(physical_drops.load(Ordering::SeqCst), 0);
4312        assert_eq!(accounting.allocated.load(Ordering::SeqCst), 64);
4313        assert!(registry
4314            .lock()
4315            .unwrap()
4316            .reserve_memory_uses(7, &[memory])
4317            .is_err());
4318
4319        let late_registry = Arc::clone(&registry);
4320        std::thread::spawn(move || {
4321            assert!(reclaim_shared_allocation(&mut pending, |owner| {
4322                crate::device_runtime::resource::with_reclamation_admission(
4323                    &late_registry,
4324                    7,
4325                    memory,
4326                    owner.reclamation.release_proof(),
4327                    &mut owner.admission,
4328                    || {
4329                        assert!(late_registry.try_lock().is_ok());
4330                        drop(owner.physical.take());
4331                        owner.reclamation.complete()
4332                    },
4333                )
4334            })
4335            .unwrap());
4336            assert!(pending.is_none());
4337        })
4338        .join()
4339        .unwrap();
4340        assert_eq!(physical_drops.load(Ordering::SeqCst), 1);
4341        assert!(reclamation.was_released());
4342        assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4343        assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4344        let replacement = AllocationReclamation::default();
4345        let group = registry
4346            .lock()
4347            .unwrap()
4348            .reserve_owned_memory_uses(&[(7, memory)], &[replacement.release_proof()])
4349            .unwrap();
4350        registry.lock().unwrap().release_memory_uses(group).unwrap();
4351    }
4352
4353    #[test]
4354    fn late_reclamation_retires_actual_payload_charge_and_same_range_together() {
4355        use crate::device_runtime::resource::{
4356            with_reclamation_admission, BlockUseRegistry, MemoryStorageOwner, MemoryUse,
4357        };
4358        use std::sync::atomic::AtomicUsize;
4359
4360        struct PhysicalStorage {
4361            _bytes: Box<[u8]>,
4362            drops: Arc<AtomicUsize>,
4363        }
4364        impl Drop for PhysicalStorage {
4365            fn drop(&mut self) {
4366                self.drops.fetch_add(1, Ordering::SeqCst);
4367            }
4368        }
4369        struct Payload {
4370            physical: Option<PhysicalStorage>,
4371            reclamation: Arc<AllocationReclamation>,
4372            drops: Arc<AtomicUsize>,
4373            admission: Option<MemoryUseGroup>,
4374        }
4375        impl Drop for Payload {
4376            fn drop(&mut self) {
4377                self.drops.fetch_add(1, Ordering::SeqCst);
4378            }
4379        }
4380        struct StorageView;
4381        impl MemoryStorageOwner for StorageView {
4382            fn dependencies(
4383                &self,
4384            ) -> crate::device_runtime::ResourceResult<Arc<DeviceAccessDependencies>> {
4385                panic!("host driver boundary must not initialize CUDA");
4386            }
4387        }
4388
4389        for enqueue_error in [false, true] {
4390            let registry = std::sync::Mutex::new(BlockUseRegistry::default());
4391            let accounting = Arc::new(GpuMemoryAccounting::default());
4392            accounting.budget_reserved.store(64, Ordering::SeqCst);
4393            let reclamation = Arc::new(AllocationReclamation::default());
4394            reclamation
4395                .attach_local(Arc::clone(&accounting), 64)
4396                .unwrap();
4397            reclamation.acquired().unwrap();
4398            let physical_drops = Arc::new(AtomicUsize::new(0));
4399            let payload_drops = Arc::new(AtomicUsize::new(0));
4400            let bytes = vec![0u8; 64].into_boxed_slice();
4401            let ptr = bytes.as_ptr() as u64;
4402            let mut live = Some(Payload {
4403                physical: Some(PhysicalStorage {
4404                    _bytes: bytes,
4405                    drops: Arc::clone(&physical_drops),
4406                }),
4407                reclamation: Arc::clone(&reclamation),
4408                drops: Arc::clone(&payload_drops),
4409                admission: None,
4410            });
4411            let view: Arc<dyn MemoryStorageOwner> = Arc::new(StorageView);
4412            let memory = MemoryUse::new(ptr, 64, Access::ReadWrite).unwrap();
4413            registry.lock().unwrap().register_storage(
4414                7,
4415                memory,
4416                Arc::downgrade(&view),
4417                reclamation.release_proof(),
4418            );
4419            let mut pending = live.take();
4420            let owner = pending.as_mut().unwrap();
4421            let result = with_reclamation_admission(
4422                &registry,
4423                7,
4424                memory,
4425                reclamation.release_proof(),
4426                &mut owner.admission,
4427                || {
4428                    assert!(registry.try_lock().is_ok());
4429                    if enqueue_error {
4430                        Err(ResourceError::Driver(
4431                            "pending free completion unknown".into(),
4432                        ))
4433                    } else {
4434                        Ok(())
4435                    }
4436                },
4437            );
4438            assert_eq!(result.is_err(), enqueue_error);
4439            drop(view);
4440            assert!(live.is_none());
4441            assert!(
4442                reclaim_allocation(&mut pending, |_| Err(ResourceError::Driver(
4443                    "driver completion still pending".into()
4444                )))
4445                .is_err()
4446            );
4447            assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4448                let _ = reclaim_allocation(&mut pending, |_| panic!("driver wait unwound"));
4449            }))
4450            .is_err());
4451            assert!(pending.as_ref().unwrap().physical.is_some());
4452            assert_eq!(physical_drops.load(Ordering::SeqCst), 0);
4453            assert_eq!(payload_drops.load(Ordering::SeqCst), 0);
4454            assert_eq!(accounting.allocated.load(Ordering::SeqCst), 64);
4455            assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 64);
4456            assert!(!reclamation.was_released());
4457            {
4458                let mut registry = registry.lock().unwrap();
4459                assert!(registry.reserve_memory_uses(7, &[memory]).is_err());
4460                assert!(registry
4461                    .retain_storage_uses(7, &[memory], &mut Vec::new())
4462                    .is_err());
4463            }
4464
4465            // The real raw-owner retirement helper runs on a different reaper.
4466            // Only the physical driver operation is replaced by host storage.
4467            std::thread::spawn(move || {
4468                reclaim_allocation(&mut pending, |owner| {
4469                    drop(owner.physical.take());
4470                    owner.reclamation.complete()
4471                })
4472                .unwrap();
4473                assert!(pending.is_none());
4474                reclaim_allocation(&mut pending, |_| panic!("must not free twice")).unwrap();
4475            })
4476            .join()
4477            .unwrap();
4478            assert_eq!(physical_drops.load(Ordering::SeqCst), 1);
4479            assert_eq!(payload_drops.load(Ordering::SeqCst), 1);
4480            assert!(reclamation.was_released());
4481            assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4482            assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4483            assert_eq!(Arc::strong_count(&accounting), 1);
4484            let replacement: Arc<dyn MemoryStorageOwner> = Arc::new(StorageView);
4485            let replacement_reclamation = AllocationReclamation::default();
4486            let mut retained = Vec::new();
4487            {
4488                let mut registry = registry.lock().unwrap();
4489                registry.register_storage(
4490                    7,
4491                    memory,
4492                    Arc::downgrade(&replacement),
4493                    replacement_reclamation.release_proof(),
4494                );
4495                // Reusing the address never revalidates an old source owner.
4496                assert!(registry
4497                    .reserve_owned_memory_uses(&[(7, memory)], &[reclamation.release_proof()],)
4498                    .is_err());
4499                registry
4500                    .retain_storage_uses(7, &[memory], &mut retained)
4501                    .unwrap();
4502                let group = registry
4503                    .reserve_owned_memory_uses(
4504                        &[(7, memory)],
4505                        &[replacement_reclamation.release_proof()],
4506                    )
4507                    .unwrap();
4508                registry.release_memory_uses(group).unwrap();
4509            }
4510            assert_eq!(retained.len(), 1);
4511            assert!(Arc::ptr_eq(&retained[0].owner, &replacement));
4512        }
4513    }
4514
4515    #[test]
4516    fn foreign_storage_retirement_retries_completion_and_releases_the_actual_owner() {
4517        use std::sync::atomic::{AtomicBool, AtomicUsize};
4518
4519        struct Producer(Arc<AtomicUsize>);
4520        impl Drop for Producer {
4521            fn drop(&mut self) {
4522                self.0.fetch_add(1, Ordering::SeqCst);
4523            }
4524        }
4525        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4526        let drops = Arc::new(AtomicUsize::new(0));
4527        let producer = Arc::new(Producer(Arc::clone(&drops)));
4528        let retained = Arc::downgrade(&producer);
4529        let ready = Arc::new(AtomicBool::new(false));
4530        let completion = Arc::clone(&ready);
4531        let reclamation = Arc::new(AllocationReclamation::default());
4532        let proof = Arc::clone(&reclamation);
4533        crate::cuda_graph::retire_resources_after_completion(
4534            Some(producer),
4535            move |_| {
4536                if completion.load(Ordering::SeqCst) {
4537                    Ok(())
4538                } else {
4539                    Err(ResourceError::Driver(
4540                        "producer use completion pending".into(),
4541                    ))
4542                }
4543            },
4544            |owner| {
4545                drop(owner.take());
4546                Ok(())
4547            },
4548            move |_| proof.complete(),
4549            |_, error| panic!("unexpected producer retirement failure: {error}"),
4550        );
4551        assert!(retained.upgrade().is_some());
4552        assert_eq!(drops.load(Ordering::SeqCst), 0);
4553        assert!(!reclamation.was_released());
4554        ready.store(true, Ordering::SeqCst);
4555        crate::cuda_graph::reap_capture_retirements();
4556        assert!(retained.upgrade().is_none());
4557        assert_eq!(drops.load(Ordering::SeqCst), 1);
4558        assert!(reclamation.was_released());
4559        crate::cuda_graph::reap_capture_retirements();
4560        assert_eq!(drops.load(Ordering::SeqCst), 1);
4561    }
4562
4563    #[test]
4564    fn foreign_storage_retirement_retries_release_publication_without_repeating_deleter() {
4565        use std::sync::atomic::AtomicUsize;
4566        struct Producer(Arc<AtomicUsize>);
4567        impl Drop for Producer {
4568            fn drop(&mut self) {
4569                self.0.fetch_add(1, Ordering::SeqCst);
4570            }
4571        }
4572        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4573        let drops = Arc::new(AtomicUsize::new(0));
4574        let accounting = Arc::new(GpuMemoryAccounting::default());
4575        let reclamation = Arc::new(AllocationReclamation::default());
4576        reclamation
4577            .attach_local(Arc::clone(&accounting), 64)
4578            .unwrap();
4579        reclamation.acquired().unwrap();
4580        let resources = (Some(Producer(Arc::clone(&drops))), Arc::clone(&reclamation));
4581        crate::cuda_graph::retire_resources_after_completion(
4582            resources,
4583            |_| Ok(()),
4584            |owners| {
4585                drop(owners.0.take());
4586                Ok(())
4587            },
4588            |owners| owners.1.complete(),
4589            |_, _| {},
4590        );
4591        assert_eq!(drops.load(Ordering::SeqCst), 1);
4592        assert!(!reclamation.was_released());
4593        accounting.allocated.store(64, Ordering::SeqCst);
4594        accounting.budget_reserved.store(64, Ordering::SeqCst);
4595        std::thread::spawn(crate::cuda_graph::reap_capture_retirements)
4596            .join()
4597            .unwrap();
4598        assert!(reclamation.was_released());
4599        assert_eq!(drops.load(Ordering::SeqCst), 1);
4600        assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4601        assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4602    }
4603
4604    #[test]
4605    fn foreign_storage_retirement_publication_unwind_keeps_retry_without_deleter() {
4606        use std::sync::atomic::{AtomicBool, AtomicUsize};
4607        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4608        let destroyed = Arc::new(AtomicUsize::new(0));
4609        let destroy_probe = Arc::clone(&destroyed);
4610        let first = Arc::new(AtomicBool::new(true));
4611        let context = Arc::new(());
4612        let retained = Arc::downgrade(&context);
4613        let reclamation = Arc::new(AllocationReclamation::default());
4614        let proof = Arc::clone(&reclamation);
4615        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4616            crate::cuda_graph::retire_resources_after_completion(
4617                context,
4618                |_| Ok(()),
4619                move |_| {
4620                    destroy_probe.fetch_add(1, Ordering::SeqCst);
4621                    Ok(())
4622                },
4623                move |_| {
4624                    if first.swap(false, Ordering::SeqCst) {
4625                        panic!("release publication interrupted");
4626                    }
4627                    proof.complete()
4628                },
4629                |_, error| panic!("unexpected destructive failure: {error}"),
4630            );
4631        }));
4632        assert!(result.is_err());
4633        assert_eq!(destroyed.load(Ordering::SeqCst), 1);
4634        assert!(retained.upgrade().is_some());
4635        assert!(!reclamation.was_released());
4636        std::thread::spawn(crate::cuda_graph::reap_capture_retirements)
4637            .join()
4638            .unwrap();
4639        assert_eq!(destroyed.load(Ordering::SeqCst), 1);
4640        assert!(reclamation.was_released());
4641        assert!(retained.upgrade().is_none());
4642    }
4643
4644    #[test]
4645    fn foreign_storage_retirement_unwinding_deleter_keeps_context_without_retry() {
4646        use std::sync::atomic::AtomicUsize;
4647
4648        struct Producer(Arc<AtomicUsize>);
4649        impl Drop for Producer {
4650            fn drop(&mut self) {
4651                self.0.fetch_add(1, Ordering::SeqCst);
4652                panic!("producer deleter interrupted");
4653            }
4654        }
4655        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4656        let drops = Arc::new(AtomicUsize::new(0));
4657        let context = Arc::new(());
4658        let retained_context = Arc::downgrade(&context);
4659        let reclamation = Arc::new(AllocationReclamation::default());
4660        let resources = (
4661            Some(Producer(Arc::clone(&drops))),
4662            Arc::clone(&reclamation),
4663            context,
4664        );
4665        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4666            crate::cuda_graph::retire_resources_after_completion(
4667                resources,
4668                |_| Ok(()),
4669                |owners| {
4670                    drop(owners.0.take());
4671                    Ok(())
4672                },
4673                |owners| owners.1.complete(),
4674                |_, error| panic!("unexpected retirement result: {error}"),
4675            );
4676        }));
4677        assert!(result.is_err());
4678        crate::cuda_graph::reap_capture_retirements();
4679        crate::cuda_graph::reap_capture_retirements();
4680        assert_eq!(drops.load(Ordering::SeqCst), 1);
4681        assert!(retained_context.upgrade().is_some());
4682        assert!(!reclamation.was_released());
4683    }
4684
4685    #[test]
4686    fn allocation_reclamation_refunds_local_charge_once_on_the_actual_reaper() {
4687        let accounting = Arc::new(GpuMemoryAccounting::default());
4688        accounting.budget_reserved.store(64, Ordering::SeqCst);
4689        let reclamation = Arc::new(AllocationReclamation::default());
4690        reclamation
4691            .attach_local(Arc::clone(&accounting), 64)
4692            .unwrap();
4693        reclamation.acquired().unwrap();
4694        let reaper = Arc::clone(&reclamation);
4695        std::thread::spawn(move || reaper.complete().unwrap())
4696            .join()
4697            .unwrap();
4698        reclamation.complete().unwrap();
4699        assert!(reclamation.was_released());
4700        assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4701        assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4702        assert_eq!(Arc::strong_count(&accounting), 1);
4703    }
4704
4705    #[test]
4706    fn allocation_reclamation_settles_all_byte_charges_before_handle_retirement() {
4707        let local = Arc::new(GpuMemoryAccounting::default());
4708        local.budget_reserved.store(64, Ordering::SeqCst);
4709        let ledger = Arc::new(AllocationAccounting::default());
4710        let pending = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4711        let ticket = Arc::new(AllocationReclamation::default());
4712        ticket.attach_local(Arc::clone(&local), 64).unwrap();
4713        ticket.attach_resource(Arc::clone(&ledger), 64).unwrap();
4714        let storage = vec![0_u8; 64].into_boxed_slice();
4715        ticket.acquired().unwrap();
4716        ticket.attach_pending(Arc::clone(&pending), 64).unwrap();
4717        assert!(ticket.attach_pending(Arc::clone(&pending), 64).is_err());
4718        assert_eq!(pending.load(Ordering::SeqCst), 64);
4719        let mut handle_owner = Some((storage, Arc::clone(&ticket)));
4720        assert!(reclaim_allocation(&mut handle_owner, |owner| {
4721            // The memory allocation, not the surrounding handle owner, is freed.
4722            drop(std::mem::take(&mut owner.0));
4723            owner.1.complete()?;
4724            Err(ResourceError::Driver(
4725                "stream destruction outcome unknown".into(),
4726            ))
4727        })
4728        .is_err());
4729        assert!(handle_owner.is_some());
4730        assert!(ticket.was_acquired());
4731        assert!(ticket.was_released());
4732        assert_eq!(local.budget_reserved.load(Ordering::SeqCst), 0);
4733        assert_eq!(local.allocated.load(Ordering::SeqCst), 0);
4734        assert_eq!(ledger.snapshot(), (0, 64));
4735        assert_eq!(pending.load(Ordering::SeqCst), 0);
4736        ticket.complete().unwrap();
4737        assert_eq!(ledger.snapshot(), (0, 64));
4738    }
4739
4740    #[test]
4741    fn allocation_reclamation_concurrent_completion_settles_once() {
4742        let ledger = Arc::new(AllocationAccounting::default());
4743        let ticket = Arc::new(AllocationReclamation::default());
4744        ticket.attach_resource(Arc::clone(&ledger), 64).unwrap();
4745        let storage = vec![0_u8; 64].into_boxed_slice();
4746        ticket.acquired().unwrap();
4747        drop(storage);
4748        let barrier = Arc::new(std::sync::Barrier::new(8));
4749        std::thread::scope(|scope| {
4750            for _ in 0..8 {
4751                let ticket = Arc::clone(&ticket);
4752                let barrier = Arc::clone(&barrier);
4753                scope.spawn(move || {
4754                    barrier.wait();
4755                    for _ in 0..100 {
4756                        ticket.complete().unwrap();
4757                    }
4758                });
4759            }
4760        });
4761        assert_eq!(ledger.snapshot(), (0, 64));
4762    }
4763
4764    #[test]
4765    fn allocation_reclamation_detach_panic_does_not_skip_physical_reap() {
4766        use crate::device_runtime::{DeviceMemoryResource, ResourceResult};
4767        struct InterruptedDetach(
4768            crate::device_runtime::budget::tests::DeferredHostResource,
4769            bool,
4770        );
4771        impl DeviceMemoryResource for InterruptedDetach {
4772            fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
4773                self.0.materialize(request)
4774            }
4775            fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
4776                self.0.allocation_accounting()
4777            }
4778            fn device_ordinal(&self) -> u32 {
4779                0
4780            }
4781            fn bytes_outstanding(&self) -> usize {
4782                self.0.bytes_outstanding()
4783            }
4784            fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
4785                self.0.deallocate(block)?;
4786                panic!("detach telemetry interrupted");
4787            }
4788            fn reap_pending(&self) -> ResourceResult<()> {
4789                let owners = std::mem::take(&mut *self.0.owners.lock().unwrap());
4790                drop(owners);
4791                if self.1 {
4792                    panic!("reap telemetry interrupted");
4793                }
4794                Ok(())
4795            }
4796        }
4797        for reap_panics in [false, true] {
4798            let resource: Arc<dyn DeviceMemoryResource + Send + Sync> =
4799                Arc::new(InterruptedDetach(Default::default(), reap_panics));
4800            let ledger = resource.allocation_accounting();
4801            let request = AllocationRequest::new(64, StreamId::DEFAULT, AllocTag::UNTAGGED);
4802            let ticket = request.reclamation();
4803            let block = resource.materialize(request).unwrap();
4804            let mut owner = ResourceBlockRetirement::new(block, resource, ticket);
4805            let outcome =
4806                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| owner.release()));
4807            let panic = outcome.expect_err("detach panic must be preserved");
4808            assert_eq!(
4809                panic.downcast_ref::<&str>(),
4810                Some(&"detach telemetry interrupted")
4811            );
4812            assert_eq!(ledger.snapshot(), (0, 64));
4813            owner.release().unwrap();
4814        }
4815    }
4816
4817    #[test]
4818    fn allocation_reclamation_partial_settlement_does_not_refund_twice() {
4819        let local = Arc::new(GpuMemoryAccounting::default());
4820        local.budget_reserved.store(64, Ordering::SeqCst);
4821        let ledger = Arc::new(AllocationAccounting::default());
4822        let pending = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4823        let ticket = AllocationReclamation::default();
4824        ticket.attach_local(Arc::clone(&local), 64).unwrap();
4825        ticket.attach_resource(Arc::clone(&ledger), 64).unwrap();
4826        ticket.acquired().unwrap();
4827        ticket.attach_pending(Arc::clone(&pending), 64).unwrap();
4828        pending.store(32, Ordering::SeqCst); // inject a failed final obligation
4829        assert!(ticket.complete().is_err());
4830        assert_eq!(local.allocated.load(Ordering::SeqCst), 0);
4831        assert_eq!(ledger.snapshot(), (0, 64));
4832        assert!(!ticket.was_released());
4833        pending.store(64, Ordering::SeqCst);
4834        ticket.complete().unwrap();
4835        assert_eq!(ledger.snapshot(), (0, 64));
4836        assert_eq!(pending.load(Ordering::SeqCst), 0);
4837        assert!(ticket.was_released());
4838    }
4839
4840    #[test]
4841    fn allocation_reclamation_local_attempt_restores_only_unacquired_claims() {
4842        for from_token in [false, true] {
4843            for acquired in [false, true] {
4844                let accounting = Arc::new(GpuMemoryAccounting::default());
4845                accounting.budget_reserved.store(128, Ordering::SeqCst);
4846                let mut remaining = 64;
4847                let mut ticket = None;
4848                let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4849                    let attempt = LocalAllocationAttempt::new(
4850                        Arc::clone(&accounting),
4851                        64,
4852                        if from_token {
4853                            Some(&mut remaining)
4854                        } else {
4855                            None
4856                        },
4857                    );
4858                    ticket = Some(Arc::clone(&attempt.reclamation));
4859                    if acquired {
4860                        attempt.reclamation.acquired().unwrap();
4861                        // A concurrent physical release before the original frame
4862                        // unwinds must not make the frame treat malloc as refused.
4863                        attempt.reclamation.complete().unwrap();
4864                    }
4865                    panic!("allocation frame interrupted");
4866                }));
4867                assert!(panic.is_err());
4868                assert_eq!(remaining, if from_token && !acquired { 128 } else { 64 });
4869                assert_eq!(
4870                    accounting.budget_reserved.load(Ordering::SeqCst),
4871                    if from_token && !acquired { 128 } else { 64 }
4872                );
4873                assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4874            }
4875        }
4876    }
4877
4878    #[test]
4879    fn allocation_reclamation_unknown_completion_retains_canonical_charge() {
4880        let accounting = Arc::new(GpuMemoryAccounting::default());
4881        accounting.allocated.store(64, Ordering::SeqCst);
4882        accounting.budget_reserved.store(64, Ordering::SeqCst);
4883        let reclamation = AllocationReclamation::default();
4884        accounting.allocated.store(0, Ordering::SeqCst);
4885        reclamation
4886            .attach_local(Arc::clone(&accounting), 64)
4887            .unwrap();
4888        reclamation.acquired().unwrap();
4889        assert!(!reclamation.was_released());
4890        drop(reclamation);
4891        assert_eq!(accounting.allocated.load(Ordering::SeqCst), 64);
4892        assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 64);
4893    }
4894
4895    #[test]
4896    fn allocation_reclamation_failed_refund_is_atomic_and_retryable() {
4897        let accounting = Arc::new(GpuMemoryAccounting::default());
4898        accounting.budget_reserved.store(64, Ordering::SeqCst);
4899        let reclamation = AllocationReclamation::default();
4900        reclamation
4901            .attach_local(Arc::clone(&accounting), 64)
4902            .unwrap();
4903        reclamation.acquired().unwrap();
4904        accounting.allocated.store(32, Ordering::SeqCst);
4905        assert!(reclamation.complete().is_err());
4906        assert!(!reclamation.was_released());
4907        assert_eq!(accounting.allocated.load(Ordering::SeqCst), 32);
4908        assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 64);
4909        assert_eq!(Arc::strong_count(&accounting), 2);
4910        accounting.allocated.store(64, Ordering::SeqCst);
4911        reclamation.complete().unwrap();
4912        assert!(reclamation.was_released());
4913        assert_eq!(accounting.allocated.load(Ordering::SeqCst), 0);
4914        assert_eq!(accounting.budget_reserved.load(Ordering::SeqCst), 0);
4915    }
4916
4917    #[test]
4918    fn physical_release_proof_is_independent_of_another_pending_failure() {
4919        assert!(allocation_reclamation_outcome(
4920            true,
4921            Err(ResourceError::Driver("another allocation failed".into()))
4922        )
4923        .is_ok());
4924        assert!(allocation_reclamation_outcome(false, Ok(())).is_err());
4925        assert!(allocation_reclamation_outcome(
4926            false,
4927            Err(ResourceError::Driver("own free failed".into()))
4928        )
4929        .is_err());
4930    }
4931    #[test]
4932    fn allocation_initialization_failure_reaches_the_canonical_cold_reaper() {
4933        use std::sync::atomic::{AtomicBool, AtomicUsize};
4934
4935        struct DeferredOwner {
4936            storage: Option<Box<[u8]>>,
4937            completed: Arc<AtomicBool>,
4938            releases: Arc<AtomicUsize>,
4939        }
4940        impl Drop for DeferredOwner {
4941            fn drop(&mut self) {
4942                let mut storage = self.storage.take();
4943                let completed = Arc::clone(&self.completed);
4944                let releases = Arc::clone(&self.releases);
4945                crate::cuda_graph::retry_retirement_after_stream_captures(move || {
4946                    if !completed.load(Ordering::Acquire) {
4947                        return false;
4948                    }
4949                    drop(storage.take());
4950                    releases.fetch_add(1, Ordering::SeqCst);
4951                    true
4952                });
4953            }
4954        }
4955
4956        let _capture_test = crate::cuda_graph::capture_lifecycle_test_guard();
4957        for unwind in [false, true] {
4958            let completed = Arc::new(AtomicBool::new(false));
4959            let releases = Arc::new(AtomicUsize::new(0));
4960            let owner = DeferredOwner {
4961                storage: Some(vec![0; 64].into_boxed_slice()),
4962                completed: Arc::clone(&completed),
4963                releases: Arc::clone(&releases),
4964            };
4965            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4966                let result = initialize_allocation(owner, 64, Arc::default(), |_| {
4967                    if unwind {
4968                        panic!("allocation initialization interrupted");
4969                    }
4970                    Err(ResourceError::Driver(
4971                        "allocation initialization failed".into(),
4972                    ))
4973                });
4974                assert!(result.is_err());
4975            }));
4976            assert_eq!(result.is_err(), unwind);
4977            crate::cuda_graph::reap_capture_retirements();
4978            assert_eq!(releases.load(Ordering::SeqCst), 0);
4979            completed.store(true, Ordering::Release);
4980            std::thread::spawn(crate::cuda_graph::reap_capture_retirements)
4981                .join()
4982                .unwrap();
4983            assert_eq!(releases.load(Ordering::SeqCst), 1);
4984            crate::cuda_graph::reap_capture_retirements();
4985            assert_eq!(releases.load(Ordering::SeqCst), 1);
4986        }
4987    }
4988
4989    #[test]
4990    fn allocation_initialization_failure_transfers_actual_owner_to_drop() {
4991        struct Owner(Arc<std::sync::atomic::AtomicUsize>);
4992        impl Drop for Owner {
4993            fn drop(&mut self) {
4994                self.0.fetch_add(1, Ordering::SeqCst);
4995            }
4996        }
4997        let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
4998        let reclamation = Arc::new(AllocationReclamation::default());
4999        reclamation.acquired().unwrap();
5000        let error = initialize_allocation(Owner(Arc::clone(&drops)), 64, reclamation, |_| {
5001            Err(ResourceError::Driver(
5002                "allocation-ready record failed".into(),
5003            ))
5004        })
5005        .err()
5006        .expect("initialization must fail");
5007        assert_eq!(error.retained_allocation_bytes(), 64);
5008        drop(error);
5009        assert_eq!(drops.load(Ordering::SeqCst), 1);
5010    }
5011
5012    #[test]
5013    fn allocation_initialization_unwind_transfers_actual_owner_to_drop() {
5014        struct Owner(Arc<std::sync::atomic::AtomicUsize>);
5015        impl Drop for Owner {
5016            fn drop(&mut self) {
5017                self.0.fetch_add(1, Ordering::SeqCst);
5018            }
5019        }
5020        let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
5021        let result = std::panic::catch_unwind(|| {
5022            let _ = initialize_allocation(Owner(Arc::clone(&drops)), 64, Arc::default(), |_| {
5023                panic!("initialization unwound")
5024            });
5025        });
5026        assert!(result.is_err());
5027        assert_eq!(drops.load(Ordering::SeqCst), 1);
5028    }
5029
5030    #[test]
5031    fn allocation_initialization_success_transfers_owner_once() {
5032        let owner = Arc::new(());
5033        let initialized =
5034            initialize_allocation(Arc::clone(&owner), 64, Arc::default(), |_| Ok(())).unwrap();
5035        assert_eq!(Arc::strong_count(&owner), 2);
5036        drop(initialized);
5037        assert_eq!(Arc::strong_count(&owner), 1);
5038    }
5039
5040    #[test]
5041    fn allocation_release_retries_only_unconfirmed_fence_steps() {
5042        use std::cell::Cell;
5043
5044        let mut state = DriverReleaseState::default();
5045        let frees = Cell::new(0);
5046        state
5047            .submit(|| {
5048                frees.set(frees.get() + 1);
5049                Ok(())
5050            })
5051            .unwrap();
5052        let records = Cell::new(0);
5053        let waits = Cell::new(0);
5054        let mut recorded = false;
5055        assert!(state
5056            .confirm_async(
5057                &mut recorded,
5058                || {
5059                    records.set(records.get() + 1);
5060                    Err(ResourceError::Driver("record failed".into()))
5061                },
5062                || {
5063                    waits.set(waits.get() + 1);
5064                    Ok(())
5065                },
5066            )
5067            .is_err());
5068        assert!(!recorded);
5069        assert_eq!(waits.get(), 0, "an unrecorded event proves no free");
5070        assert!(state
5071            .confirm_async(
5072                &mut recorded,
5073                || {
5074                    records.set(records.get() + 1);
5075                    Ok(())
5076                },
5077                || {
5078                    waits.set(waits.get() + 1);
5079                    Err(ResourceError::Driver("wait failed".into()))
5080                },
5081            )
5082            .is_err());
5083        assert!(recorded);
5084        state
5085            .confirm_async(
5086                &mut recorded,
5087                || panic!("recorded fence must not be replaced"),
5088                || {
5089                    waits.set(waits.get() + 1);
5090                    Ok(())
5091                },
5092            )
5093            .unwrap();
5094        assert_eq!(frees.get(), 1);
5095        assert_eq!(records.get(), 2);
5096        assert_eq!(waits.get(), 2);
5097
5098        for state in [
5099            DriverReleaseState::Owned,
5100            DriverReleaseState::OutcomeUnknown,
5101        ] {
5102            assert!(state
5103                .confirm_async(
5104                    &mut false,
5105                    || panic!("cannot invent proof of an unconfirmed free submission"),
5106                    || panic!("cannot confirm an unsubmitted free"),
5107                )
5108                .is_err());
5109        }
5110    }
5111
5112    #[test]
5113    fn allocation_release_does_not_resubmit_unknown_free() {
5114        let attempts = std::cell::Cell::new(0);
5115        let mut state = DriverReleaseState::default();
5116        assert!(state
5117            .submit(|| {
5118                attempts.set(attempts.get() + 1);
5119                Err(ResourceError::Driver("unknown free outcome".into()))
5120            })
5121            .is_err());
5122        assert!(state
5123            .submit(|| {
5124                attempts.set(attempts.get() + 1);
5125                Ok(())
5126            })
5127            .is_err());
5128        assert_eq!(attempts.get(), 1);
5129    }
5130
5131    #[test]
5132    fn allocation_release_does_not_resubmit_successful_free() {
5133        let attempts = std::cell::Cell::new(0);
5134        let mut state = DriverReleaseState::default();
5135        for _ in 0..2 {
5136            state
5137                .submit(|| {
5138                    attempts.set(attempts.get() + 1);
5139                    Ok(())
5140                })
5141                .unwrap();
5142        }
5143        assert_eq!(attempts.get(), 1);
5144    }
5145    use super::*;
5146    use crate::device_runtime::{DeviceMemoryResource, DirectCudaResource, ResourceResult};
5147    use xlog_core::ScalarType;
5148
5149    #[test]
5150    fn reservation_exposes_exact_memory_manager_identity() {
5151        let _: fn(&GpuMemoryReservation) -> usize = GpuMemoryReservation::memory_manager_ptr_value;
5152    }
5153
5154    #[test]
5155    fn cuda_column_exposes_optional_memory_manager_identity() {
5156        let _: fn(&CudaColumn) -> Option<usize> = CudaColumn::memory_manager_ptr_value;
5157    }
5158
5159    #[test]
5160    fn tracked_slice_exposes_runtime_allocation_identity_snapshot() {
5161        let _: fn(&TrackedCudaSlice<u32>) -> Result<Option<RuntimeAllocationIdentity>> =
5162            TrackedCudaSlice::<u32>::runtime_allocation_identity;
5163    }
5164
5165    #[test]
5166    fn cuda_column_exposes_runtime_allocation_identity_snapshot() {
5167        let _: fn(&CudaColumn) -> Result<Option<RuntimeAllocationIdentity>> =
5168            CudaColumn::runtime_allocation_identity;
5169    }
5170
5171    struct FailAfterDeallocateResource {
5172        inner: DirectCudaResource,
5173        deallocate_calls: Arc<AtomicU64>,
5174    }
5175
5176    struct FailFirstAllocationResource {
5177        inner: DirectCudaResource,
5178        fail_next: std::sync::atomic::AtomicBool,
5179    }
5180
5181    impl DeviceMemoryResource for FailFirstAllocationResource {
5182        fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
5183            if self.fail_next.swap(false, Ordering::SeqCst) {
5184                return Err(ResourceError::Driver(
5185                    "injected allocation failure".to_string(),
5186                ));
5187            }
5188            self.inner.materialize(request)
5189        }
5190
5191        fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
5192            self.inner.allocation_accounting()
5193        }
5194
5195        fn access_dependencies(
5196            &self,
5197            block: BlockId,
5198            bytes: usize,
5199        ) -> ResourceResult<Option<Arc<DeviceAccessDependencies>>> {
5200            self.inner.access_dependencies(block, bytes)
5201        }
5202
5203        fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
5204            self.inner.deallocate(block)
5205        }
5206
5207        fn device_ordinal(&self) -> u32 {
5208            self.inner.device_ordinal()
5209        }
5210
5211        fn bytes_outstanding(&self) -> usize {
5212            self.inner.bytes_outstanding()
5213        }
5214    }
5215
5216    impl DeviceMemoryResource for FailAfterDeallocateResource {
5217        fn materialize(&self, request: AllocationRequest) -> ResourceResult<DeviceBlock> {
5218            self.inner.materialize(request)
5219        }
5220
5221        fn allocation_accounting(&self) -> Arc<AllocationAccounting> {
5222            self.inner.allocation_accounting()
5223        }
5224
5225        fn access_dependencies(
5226            &self,
5227            block: BlockId,
5228            bytes: usize,
5229        ) -> ResourceResult<Option<Arc<DeviceAccessDependencies>>> {
5230            self.inner.access_dependencies(block, bytes)
5231        }
5232
5233        fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
5234            self.inner.deallocate(block)?;
5235            self.deallocate_calls.fetch_add(1, Ordering::SeqCst);
5236            Err(ResourceError::Driver(
5237                "injected deallocation completion failure".to_string(),
5238            ))
5239        }
5240
5241        fn device_ordinal(&self) -> u32 {
5242            self.inner.device_ordinal()
5243        }
5244
5245        fn bytes_outstanding(&self) -> usize {
5246            self.inner.bytes_outstanding()
5247        }
5248    }
5249
5250    fn try_device() -> Option<Arc<CudaDevice>> {
5251        match CudaDevice::new(0) {
5252            Ok(d) => Some(Arc::new(d)),
5253            Err(e) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
5254                panic!("XLOG_REQUIRE_CUDA=1 but CUDA initialization failed: {e}")
5255            }
5256            Err(e) => {
5257                eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
5258                None
5259            }
5260        }
5261    }
5262
5263    fn assert_memory_pressure(
5264        error: XlogError,
5265        expected_context: &str,
5266        expected_required: u64,
5267        expected_budget: u64,
5268    ) {
5269        match error {
5270            XlogError::ResourceExhausted {
5271                context,
5272                estimated_bytes,
5273                budget_bytes,
5274            } => {
5275                assert_eq!(context, expected_context);
5276                assert_eq!(estimated_bytes, expected_required);
5277                assert_eq!(budget_bytes, expected_budget);
5278            }
5279            other => panic!("expected ResourceExhausted, got {other:?}"),
5280        }
5281    }
5282
5283    // Test CudaBuffer without requiring a GPU
5284    #[test]
5285    fn test_cuda_buffer_empty() {
5286        let Some(device) = try_device() else {
5287            return;
5288        };
5289        let budget = MemoryBudget::with_limit(1024 * 1024);
5290        let manager = Arc::new(GpuMemoryManager::new(device, budget));
5291        let mut d_num_rows = manager.alloc::<u32>(1).unwrap();
5292        manager
5293            .device()
5294            .inner()
5295            .htod_sync_copy_into(&[0u32], &mut d_num_rows)
5296            .unwrap();
5297        let buffer = CudaBuffer::from_columns(Vec::new(), 0, d_num_rows, Schema::new(vec![]));
5298        assert!(buffer.is_empty());
5299        assert_eq!(buffer.num_rows(), 0);
5300        assert_eq!(buffer.arity(), 0);
5301        assert_eq!(buffer.estimated_bytes(), 0);
5302    }
5303
5304    #[test]
5305    fn test_cuda_buffer_schema() {
5306        let schema = Schema::new(vec![
5307            ("a".to_string(), ScalarType::U32),
5308            ("b".to_string(), ScalarType::U64),
5309        ]);
5310
5311        let Some(device) = try_device() else {
5312            return;
5313        };
5314        let budget = MemoryBudget::with_limit(1024 * 1024);
5315        let manager = Arc::new(GpuMemoryManager::new(device, budget));
5316        let mut d_num_rows = manager.alloc::<u32>(1).unwrap();
5317        manager
5318            .device()
5319            .inner()
5320            .htod_sync_copy_into(&[100u32], &mut d_num_rows)
5321            .unwrap();
5322
5323        // Allocate dummy columns matching the schema arity (100 rows each)
5324        let col_a = CudaColumn::owned(manager.alloc::<u8>(100 * 4).unwrap()); // U32: 4 bytes
5325        let col_b = CudaColumn::owned(manager.alloc::<u8>(100 * 8).unwrap()); // U64: 8 bytes
5326        let buffer = CudaBuffer::from_columns(vec![col_a, col_b], 100, d_num_rows, schema.clone());
5327
5328        assert_eq!(buffer.num_rows(), 100);
5329        assert_eq!(buffer.arity(), 2);
5330        // 4 bytes (U32) + 8 bytes (U64) = 12 bytes per row * 100 rows
5331        assert_eq!(buffer.estimated_bytes(), 1200);
5332        assert_eq!(buffer.schema(), &schema);
5333    }
5334
5335    // Tests requiring GPU
5336    #[test]
5337    fn test_memory_manager_creation() {
5338        let Some(device) = try_device() else {
5339            return;
5340        };
5341        let budget = MemoryBudget::with_limit(1024 * 1024); // 1 MB
5342        let manager = Arc::new(GpuMemoryManager::new(device, budget));
5343
5344        assert_eq!(manager.allocated_bytes(), 0);
5345        assert_eq!(manager.budget().device_bytes, 1024 * 1024);
5346        assert_eq!(manager.remaining_bytes(), 1024 * 1024);
5347    }
5348
5349    #[test]
5350    fn test_memory_manager_alloc() {
5351        let Some(device) = try_device() else {
5352            return;
5353        };
5354        let budget = MemoryBudget::with_limit(1024 * 1024); // 1 MB
5355        let manager = Arc::new(GpuMemoryManager::new(device, budget));
5356
5357        // Allocate 256 u32 values = 1024 bytes
5358        let _slice = manager
5359            .alloc::<u32>(256)
5360            .expect("Allocation should succeed");
5361
5362        assert_eq!(manager.allocated_bytes(), 1024);
5363        assert_eq!(manager.remaining_bytes(), 1024 * 1024 - 1024);
5364    }
5365
5366    #[test]
5367    fn test_memory_manager_budget_exceeded() {
5368        let Some(device) = try_device() else {
5369            return;
5370        };
5371        let budget = MemoryBudget::with_limit(1024); // 1 KB limit
5372        let manager = Arc::new(GpuMemoryManager::new(device, budget));
5373
5374        // Try to allocate 512 u32 values = 2048 bytes (exceeds 1KB budget)
5375        let result = manager.alloc::<u32>(512);
5376
5377        assert!(result.is_err());
5378        if let Err(XlogError::ResourceExhausted {
5379            estimated_bytes,
5380            budget_bytes,
5381            ..
5382        }) = result
5383        {
5384            assert_eq!(estimated_bytes, 2048);
5385            assert_eq!(budget_bytes, 1024);
5386        } else {
5387            panic!("Expected ResourceExhausted error");
5388        }
5389    }
5390
5391    #[test]
5392    fn test_memory_manager_check_budget() {
5393        let Some(device) = try_device() else {
5394            return;
5395        };
5396        let budget = MemoryBudget::with_limit(1000);
5397        let manager = Arc::new(GpuMemoryManager::new(device, budget));
5398
5399        // Check that 500 bytes is within budget
5400        assert!(manager.check_budget(500).is_ok());
5401
5402        // Check that 1001 bytes exceeds budget
5403        assert!(manager.check_budget(1001).is_err());
5404    }
5405
5406    #[test]
5407    fn check_budget_does_not_reserve_a_multi_allocation_request() {
5408        let Some(device) = try_device() else {
5409            return;
5410        };
5411        let manager = Arc::new(GpuMemoryManager::new(
5412            device,
5413            MemoryBudget::with_limit(8192),
5414        ));
5415        let checked = Arc::new(std::sync::Barrier::new(2));
5416        let competitor_allocated = Arc::new(std::sync::Barrier::new(2));
5417        let release_competitor = Arc::new(std::sync::Barrier::new(2));
5418
5419        let checked_in_thread = Arc::clone(&checked);
5420        let allocated_in_thread = Arc::clone(&competitor_allocated);
5421        let release_in_thread = Arc::clone(&release_competitor);
5422        let competitor_manager = Arc::clone(&manager);
5423        let competitor = std::thread::spawn(move || {
5424            checked_in_thread.wait();
5425            let allocation = competitor_manager
5426                .alloc::<u8>(4096)
5427                .expect("competing allocation must fit after the check-only query");
5428            allocated_in_thread.wait();
5429            release_in_thread.wait();
5430            drop(allocation);
5431        });
5432
5433        manager
5434            .check_budget(8192)
5435            .expect("the complete request fits before the competing allocation");
5436        checked.wait();
5437        competitor_allocated.wait();
5438        let first = manager
5439            .alloc::<u8>(4096)
5440            .expect("the first materialized allocation must fit");
5441        let second = manager.alloc::<u8>(4096);
5442        assert!(
5443            matches!(second, Err(XlogError::ResourceExhausted { .. })),
5444            "a check-only query cannot protect later allocations from a competitor"
5445        );
5446
5447        release_competitor.wait();
5448        competitor.join().expect("competitor thread panicked");
5449        drop(first);
5450        assert_eq!(manager.allocated_bytes(), 0);
5451        assert_eq!(manager.remaining_bytes(), 8192);
5452    }
5453
5454    #[test]
5455    fn reservation_rejects_the_complete_request_before_any_allocation() {
5456        let Some(device) = try_device() else {
5457            return;
5458        };
5459        let manager = Arc::new(GpuMemoryManager::new(
5460            device,
5461            MemoryBudget::with_limit(4095),
5462        ));
5463        manager.reset_alloc_count();
5464
5465        let error = manager
5466            .reserve_bytes(4096)
5467            .expect_err("a request one byte above the budget must be rejected atomically");
5468
5469        assert_memory_pressure(
5470            error,
5471            "GPU memory pressure: layer=manager_reserve current_bytes=0 requested_bytes=4096 required_bytes=4096 required_u64_overflow=false budget_bytes=4095 prior_peak_bytes=0",
5472            4096,
5473            4095,
5474        );
5475        assert_eq!(manager.alloc_count(), 0);
5476        assert_eq!(manager.allocated_bytes(), 0);
5477        assert_eq!(manager.peak_bytes(), 0);
5478        assert_eq!(manager.remaining_bytes(), 4095);
5479    }
5480
5481    #[test]
5482    fn competing_complete_reservations_cannot_both_claim_the_budget() {
5483        let Some(device) = try_device() else {
5484            return;
5485        };
5486        let manager = Arc::new(GpuMemoryManager::new(
5487            device,
5488            MemoryBudget::with_limit(4096),
5489        ));
5490        let attempted = Arc::new(std::sync::Barrier::new(2));
5491
5492        let reserve = |manager: Arc<GpuMemoryManager>, attempted: Arc<std::sync::Barrier>| {
5493            std::thread::spawn(move || {
5494                let reservation = manager.reserve_bytes(4096);
5495                attempted.wait();
5496                let admitted = reservation.is_ok();
5497                drop(reservation);
5498                admitted
5499            })
5500        };
5501        let left = reserve(Arc::clone(&manager), Arc::clone(&attempted));
5502        let right = reserve(Arc::clone(&manager), Arc::clone(&attempted));
5503
5504        let admitted = [
5505            left.join().expect("left reservation thread panicked"),
5506            right.join().expect("right reservation thread panicked"),
5507        ];
5508        assert_eq!(admitted.into_iter().filter(|value| *value).count(), 1);
5509        assert_eq!(manager.allocated_bytes(), 0);
5510        assert_eq!(manager.remaining_bytes(), 4096);
5511    }
5512
5513    #[test]
5514    fn competing_managers_share_one_runtime_reservation_budget() {
5515        let Some((device, runtime)) = try_runtime_with_budget(4096) else {
5516            return;
5517        };
5518        let left_manager = Arc::new(GpuMemoryManager::with_runtime(
5519            Arc::clone(&device),
5520            MemoryBudget::with_limit(4096),
5521            Arc::clone(&runtime),
5522        ));
5523        let right_manager = Arc::new(GpuMemoryManager::with_runtime(
5524            device,
5525            MemoryBudget::with_limit(4096),
5526            runtime,
5527        ));
5528        let attempted = Arc::new(std::sync::Barrier::new(2));
5529
5530        let reserve = |manager: Arc<GpuMemoryManager>, attempted: Arc<std::sync::Barrier>| {
5531            std::thread::spawn(move || {
5532                let reservation = manager.reserve_bytes(4096);
5533                attempted.wait();
5534                let admitted = reservation.is_ok();
5535                drop(reservation);
5536                admitted
5537            })
5538        };
5539        let left = reserve(left_manager, Arc::clone(&attempted));
5540        let right = reserve(right_manager, attempted);
5541
5542        let admitted = [
5543            left.join().expect("left reservation thread panicked"),
5544            right.join().expect("right reservation thread panicked"),
5545        ];
5546        assert_eq!(admitted.into_iter().filter(|value| *value).count(), 1);
5547    }
5548
5549    #[test]
5550    fn runtime_reservation_blocks_competing_ordinary_allocation_until_materialized() {
5551        let Some((device, runtime)) = try_runtime_with_budget(4096) else {
5552            return;
5553        };
5554        let reserving_manager = Arc::new(GpuMemoryManager::with_runtime(
5555            Arc::clone(&device),
5556            MemoryBudget::with_limit(4096),
5557            Arc::clone(&runtime),
5558        ));
5559        let competing_manager = Arc::new(GpuMemoryManager::with_runtime(
5560            device,
5561            MemoryBudget::with_limit(4096),
5562            runtime,
5563        ));
5564        let mut reservation = reserving_manager
5565            .reserve_bytes(4096)
5566            .expect("complete runtime reservation");
5567
5568        assert!(matches!(
5569            competing_manager.alloc::<u8>(1),
5570            Err(XlogError::ResourceExhausted { .. })
5571        ));
5572        assert_eq!(competing_manager.allocated_bytes(), 0);
5573        assert_eq!(competing_manager.remaining_bytes(), 4096);
5574
5575        let allocation = reservation
5576            .alloc::<u8>(4096)
5577            .expect("reserved bytes cannot be stolen by an ordinary allocator");
5578        assert_eq!(reserving_manager.allocated_bytes(), 4096);
5579        drop(reservation);
5580        drop(allocation);
5581        assert_eq!(reserving_manager.allocated_bytes(), 0);
5582    }
5583
5584    #[test]
5585    fn global_budget_rejects_complete_manifest_before_first_inner_allocation() {
5586        let Some((device, runtime, sink)) = try_runtime_with_logging_budget(4095) else {
5587            return;
5588        };
5589        let manager = Arc::new(GpuMemoryManager::with_runtime(
5590            device,
5591            MemoryBudget::with_limit(8192),
5592            runtime,
5593        ));
5594
5595        let error = manager
5596            .reserve_bytes(4096)
5597            .expect_err("the complete request is one byte above the runtime budget");
5598        assert_memory_pressure(
5599            error,
5600            "GPU memory pressure: layer=device_runtime current_bytes=0 requested_bytes=4096 required_bytes=4096 required_u64_overflow=false budget_bytes=4095 prior_peak_bytes=0",
5601            4096,
5602            4095,
5603        );
5604        assert!(sink.snapshot().is_empty());
5605        assert_eq!(manager.allocated_bytes(), 0);
5606        assert_eq!(manager.remaining_bytes(), 8192);
5607    }
5608
5609    #[test]
5610    fn runtime_backed_reservation_requires_a_reservable_global_budget() {
5611        let Some((device, runtime)) = try_unbudgeted_runtime() else {
5612            return;
5613        };
5614        let manager = Arc::new(GpuMemoryManager::with_runtime(
5615            device,
5616            MemoryBudget::with_limit(4096),
5617            runtime,
5618        ));
5619
5620        let error = manager
5621            .reserve_bytes(1024)
5622            .expect_err("an unbudgeted runtime cannot promise complete admission");
5623        assert!(
5624            matches!(error, XlogError::Kernel(ref detail) if detail.contains("reservable global budget")),
5625            "unexpected error: {error}"
5626        );
5627        assert_eq!(manager.allocated_bytes(), 0);
5628        assert_eq!(manager.remaining_bytes(), 4096);
5629    }
5630
5631    #[test]
5632    fn reservation_materializes_exactly_its_declared_bytes() {
5633        let Some(device) = try_device() else {
5634            return;
5635        };
5636        let manager = Arc::new(GpuMemoryManager::new(
5637            device,
5638            MemoryBudget::with_limit(4096),
5639        ));
5640        let mut reservation = manager
5641            .reserve_bytes(4096)
5642            .expect("the exact complete request must fit");
5643        assert_eq!(reservation.total_bytes(), 4096);
5644        assert_eq!(reservation.remaining_bytes(), 4096);
5645        assert_eq!(reservation.used_bytes(), 0);
5646        assert_eq!(manager.remaining_bytes(), 0);
5647
5648        let words = reservation
5649            .alloc::<u32>(512)
5650            .expect("first reserved allocation");
5651        let bytes = reservation
5652            .alloc::<u8>(2048)
5653            .expect("second reserved allocation");
5654        assert_eq!(reservation.remaining_bytes(), 0);
5655        assert_eq!(reservation.used_bytes(), 4096);
5656        assert_eq!(manager.allocated_bytes(), 4096);
5657        assert_eq!(manager.peak_bytes(), 4096);
5658
5659        let error = match reservation.alloc::<u8>(1) {
5660            Err(error) => error,
5661            Ok(_) => panic!("a reservation cannot materialize more than its declaration"),
5662        };
5663        assert_memory_pressure(
5664            error,
5665            "GPU memory pressure: layer=manager_reservation_alloc current_bytes=4096 requested_bytes=1 required_bytes=4097 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=4096",
5666            4097,
5667            4096,
5668        );
5669
5670        drop(reservation);
5671        assert_eq!(manager.remaining_bytes(), 0);
5672        drop(bytes);
5673        assert_eq!(manager.remaining_bytes(), 2048);
5674        drop(words);
5675        assert_eq!(manager.allocated_bytes(), 0);
5676        assert_eq!(manager.remaining_bytes(), 4096);
5677    }
5678
5679    #[test]
5680    fn failed_reserved_raw_allocation_preserves_unused_claim_until_drop() {
5681        let Some((device, runtime)) = try_runtime_with_first_allocation_failure(4096) else {
5682            return;
5683        };
5684        let manager = Arc::new(GpuMemoryManager::with_runtime(
5685            device,
5686            MemoryBudget::with_limit(4096),
5687            runtime,
5688        ));
5689        let mut reservation = manager
5690            .reserve_bytes(4096)
5691            .expect("local complete request must fit");
5692
5693        let error = reservation
5694            .alloc_raw(2048, AllocTag::UNTAGGED)
5695            .expect_err("the injected underlying allocation must fail");
5696        assert!(
5697            matches!(error, XlogError::Kernel(ref detail) if detail.contains("injected allocation failure")),
5698            "unexpected error: {error}"
5699        );
5700        assert_eq!(reservation.remaining_bytes(), 4096);
5701        assert_eq!(manager.allocated_bytes(), 0);
5702        assert_eq!(manager.remaining_bytes(), 0);
5703
5704        let allocation = reservation
5705            .alloc_raw(1024, AllocTag::UNTAGGED)
5706            .expect("a smaller raw suballocation must fit both budgets");
5707        assert_eq!(reservation.remaining_bytes(), 3072);
5708        assert_eq!(manager.allocated_bytes(), 1024);
5709
5710        drop(reservation);
5711        assert_eq!(manager.remaining_bytes(), 3072);
5712        drop(allocation);
5713        assert_eq!(manager.allocated_bytes(), 0);
5714        assert_eq!(manager.remaining_bytes(), 4096);
5715    }
5716
5717    #[test]
5718    fn partial_typed_materialization_releases_each_byte_once_in_either_drop_order() {
5719        let Some(device) = try_device() else {
5720            return;
5721        };
5722        let manager = Arc::new(GpuMemoryManager::new(
5723            device,
5724            MemoryBudget::with_limit(4096),
5725        ));
5726        let mut reservation = manager.reserve_bytes(4096).expect("complete request");
5727        let allocation = reservation
5728            .alloc::<u8>(1024)
5729            .expect("partial materialization");
5730        let error = match reservation.alloc::<u8>(4096) {
5731            Err(error) => error,
5732            Ok(_) => panic!("the remaining reservation is only 3072 bytes"),
5733        };
5734        assert_memory_pressure(
5735            error,
5736            "GPU memory pressure: layer=manager_reservation_alloc current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
5737            5120,
5738            4096,
5739        );
5740        assert_eq!(reservation.remaining_bytes(), 3072);
5741
5742        drop(allocation);
5743        assert_eq!(manager.allocated_bytes(), 0);
5744        assert_eq!(manager.remaining_bytes(), 1024);
5745        drop(reservation);
5746        assert_eq!(manager.remaining_bytes(), 4096);
5747    }
5748
5749    #[test]
5750    fn ordinary_allocations_and_complete_reservations_share_one_budget() {
5751        let Some(device) = try_device() else {
5752            return;
5753        };
5754        let manager = Arc::new(GpuMemoryManager::new(
5755            device,
5756            MemoryBudget::with_limit(4096),
5757        ));
5758        let ordinary = manager
5759            .alloc::<u8>(1024)
5760            .expect("ordinary allocation before reservation");
5761        let mut reservation = manager
5762            .reserve_bytes(3072)
5763            .expect("reservation must fit beside the ordinary owner");
5764        assert_eq!(manager.remaining_bytes(), 0);
5765        assert!(matches!(
5766            manager.alloc::<u8>(1),
5767            Err(XlogError::ResourceExhausted { .. })
5768        ));
5769
5770        let reserved = reservation
5771            .alloc::<u8>(3072)
5772            .expect("reserved allocation bypasses the already-paid local claim");
5773        assert_eq!(manager.allocated_bytes(), 4096);
5774        drop(reservation);
5775        drop(reserved);
5776        assert_eq!(manager.allocated_bytes(), 1024);
5777        assert_eq!(manager.remaining_bytes(), 3072);
5778        drop(ordinary);
5779        assert_eq!(manager.allocated_bytes(), 0);
5780        assert_eq!(manager.remaining_bytes(), 4096);
5781    }
5782
5783    #[test]
5784    fn public_accounting_mutators_refuse_live_reservations_and_allocations() {
5785        let Some(device) = try_device() else {
5786            return;
5787        };
5788        let manager = Arc::new(GpuMemoryManager::new(
5789            device,
5790            MemoryBudget::with_limit(4096),
5791        ));
5792        let reservation = manager.reserve_bytes(2048).expect("reservation");
5793
5794        let free_error = manager
5795            .record_free(1)
5796            .expect_err("public accounting cannot release a live reservation");
5797        assert!(
5798            matches!(free_error, XlogError::Kernel(detail) if detail.contains("live tracked bytes"))
5799        );
5800        let reset_error = manager
5801            .reset_tracking()
5802            .expect_err("public reset cannot erase a live reservation");
5803        assert!(
5804            matches!(reset_error, XlogError::Kernel(detail) if detail.contains("live tracked bytes"))
5805        );
5806        assert_eq!(manager.remaining_bytes(), 2048);
5807
5808        drop(reservation);
5809        let allocation = manager.alloc::<u8>(1024).expect("allocation");
5810        assert!(manager.record_free(1024).is_err());
5811        assert!(manager.reset_tracking().is_err());
5812        assert_eq!(manager.allocated_bytes(), 1024);
5813        assert_eq!(manager.remaining_bytes(), 3072);
5814
5815        drop(allocation);
5816        manager
5817            .reset_tracking()
5818            .expect("quiescent accounting can reset diagnostic peaks");
5819        assert_eq!(manager.allocated_bytes(), 0);
5820        assert_eq!(manager.remaining_bytes(), 4096);
5821    }
5822
5823    #[test]
5824    fn public_record_free_refuses_underflow_without_mutation() {
5825        let Some(device) = try_device() else {
5826            return;
5827        };
5828        let manager = Arc::new(GpuMemoryManager::new(
5829            device,
5830            MemoryBudget::with_limit(4096),
5831        ));
5832
5833        let error = manager
5834            .record_free(1)
5835            .expect_err("unowned bytes cannot be released from accounting");
5836        assert!(matches!(error, XlogError::Kernel(detail) if detail.contains("underflow")));
5837        assert_eq!(manager.allocated_bytes(), 0);
5838        assert_eq!(manager.remaining_bytes(), 4096);
5839        manager
5840            .record_free(0)
5841            .expect("zero-byte release is a no-op");
5842    }
5843
5844    #[test]
5845    fn typed_post_release_error_honors_physical_release_proof() {
5846        let Some((device, runtime, deallocate_calls)) = try_runtime_with_deallocation_failure()
5847        else {
5848            return;
5849        };
5850        let manager = Arc::new(GpuMemoryManager::with_runtime(
5851            device,
5852            MemoryBudget::with_limit(4096),
5853            Arc::clone(&runtime),
5854        ));
5855        let allocation = manager.alloc::<u8>(1024).expect("typed allocation");
5856
5857        drop(allocation);
5858
5859        assert_eq!(deallocate_calls.load(Ordering::SeqCst), 1);
5860        assert_eq!(runtime.bytes_outstanding(), 0);
5861        assert_eq!(manager.allocated_bytes(), 0);
5862        assert_eq!(manager.remaining_bytes(), 4096);
5863        assert_eq!(manager.deallocation_failure_count(), 0);
5864        assert_eq!(manager.deallocation_failure_bytes(), 0);
5865        manager.reset_tracking().unwrap();
5866    }
5867
5868    #[test]
5869    fn raw_post_release_error_honors_physical_release_proof() {
5870        let Some((device, runtime, deallocate_calls)) = try_runtime_with_deallocation_failure()
5871        else {
5872            return;
5873        };
5874        let manager = Arc::new(GpuMemoryManager::with_runtime(
5875            device,
5876            MemoryBudget::with_limit(4096),
5877            Arc::clone(&runtime),
5878        ));
5879        let allocation = manager
5880            .alloc_raw(512, AllocTag::UNTAGGED)
5881            .expect("raw allocation");
5882
5883        drop(allocation);
5884
5885        assert_eq!(deallocate_calls.load(Ordering::SeqCst), 1);
5886        assert_eq!(runtime.bytes_outstanding(), 0);
5887        assert_eq!(manager.allocated_bytes(), 0);
5888        assert_eq!(manager.remaining_bytes(), 4096);
5889        assert_eq!(manager.deallocation_failure_count(), 0);
5890        assert_eq!(manager.deallocation_failure_bytes(), 0);
5891        manager.reset_tracking().unwrap();
5892    }
5893
5894    #[test]
5895    fn test_memory_manager_multiple_allocs() {
5896        let Some(device) = try_device() else {
5897            return;
5898        };
5899        let budget = MemoryBudget::with_limit(4096); // 4 KB
5900        let manager = Arc::new(GpuMemoryManager::new(device, budget));
5901
5902        // First allocation: 256 u32 = 1024 bytes
5903        let _slice1 = manager
5904            .alloc::<u32>(256)
5905            .expect("First allocation should succeed");
5906        assert_eq!(manager.allocated_bytes(), 1024);
5907
5908        // Second allocation: 256 u32 = 1024 bytes
5909        let _slice2 = manager
5910            .alloc::<u32>(256)
5911            .expect("Second allocation should succeed");
5912        assert_eq!(manager.allocated_bytes(), 2048);
5913
5914        // Third allocation that would exceed budget
5915        let result = manager.alloc::<u32>(1024); // 4096 bytes, would make total 6144
5916        assert!(result.is_err());
5917
5918        // Allocated should still be 2048
5919        assert_eq!(manager.allocated_bytes(), 2048);
5920    }
5921
5922    #[test]
5923    fn test_memory_manager_record_free() {
5924        let Some(device) = try_device() else {
5925            return;
5926        };
5927        let budget = MemoryBudget::with_limit(4096);
5928        let manager = Arc::new(GpuMemoryManager::new(device, budget));
5929
5930        // Allocate
5931        let slice = manager
5932            .alloc::<u32>(256)
5933            .expect("Allocation should succeed");
5934        assert_eq!(manager.allocated_bytes(), 1024);
5935
5936        // Drop should automatically update tracking
5937        drop(slice);
5938        assert_eq!(manager.allocated_bytes(), 0);
5939        assert_eq!(manager.remaining_bytes(), 4096);
5940    }
5941
5942    #[test]
5943    fn test_memory_manager_peak_tracking() {
5944        let Some(device) = try_device() else {
5945            return;
5946        };
5947        let budget = MemoryBudget::with_limit(8192);
5948        let manager = Arc::new(GpuMemoryManager::new(device, budget));
5949
5950        let a = manager.alloc::<u32>(256).expect("alloc a"); // 1024 B
5951        let b = manager.alloc::<u32>(512).expect("alloc b"); // 2048 B
5952        assert_eq!(manager.peak_bytes(), 3072);
5953
5954        // Frees lower `allocated` but never the peak.
5955        drop(b);
5956        assert_eq!(manager.allocated_bytes(), 1024);
5957        assert_eq!(manager.peak_bytes(), 3072);
5958
5959        // reset_peak restarts the window from live state.
5960        manager.reset_peak();
5961        assert_eq!(manager.peak_bytes(), 1024);
5962
5963        let c = manager.alloc::<u32>(128).expect("alloc c"); // 512 B
5964        assert_eq!(manager.peak_bytes(), 1536);
5965
5966        drop(c);
5967        drop(a);
5968        assert_eq!(manager.allocated_bytes(), 0);
5969        assert_eq!(manager.peak_bytes(), 1536);
5970    }
5971
5972    #[test]
5973    fn memory_pressure_alloc_reports_exact_cumulative_pressure() {
5974        let Some(device) = try_device() else {
5975            return;
5976        };
5977        let manager = Arc::new(GpuMemoryManager::new(
5978            device,
5979            MemoryBudget::with_limit(4096),
5980        ));
5981        let baseline = manager.alloc::<u8>(1024).expect("baseline allocation");
5982
5983        let error = match manager.alloc::<u8>(4096) {
5984            Err(error) => error,
5985            Ok(_) => panic!("cumulative allocation must exceed the local budget"),
5986        };
5987
5988        assert_memory_pressure(
5989            error,
5990            "GPU memory pressure: layer=manager_alloc current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
5991            5120,
5992            4096,
5993        );
5994        assert_eq!(manager.allocated_bytes(), 1024);
5995        assert_eq!(manager.peak_bytes(), 1024, "refusal must not raise peak");
5996        drop(baseline);
5997    }
5998
5999    #[test]
6000    fn memory_pressure_check_budget_reports_exact_cumulative_pressure() {
6001        let Some(device) = try_device() else {
6002            return;
6003        };
6004        let manager = Arc::new(GpuMemoryManager::new(
6005            device,
6006            MemoryBudget::with_limit(1000),
6007        ));
6008        let baseline = manager.alloc::<u8>(512).expect("baseline allocation");
6009
6010        let error = manager
6011            .check_budget(600)
6012            .expect_err("cumulative request must exceed the local budget");
6013
6014        assert_memory_pressure(
6015            error,
6016            "GPU memory pressure: layer=manager_check_budget current_bytes=512 requested_bytes=600 required_bytes=1112 required_u64_overflow=false budget_bytes=1000 prior_peak_bytes=512",
6017            1112,
6018            1000,
6019        );
6020        assert_eq!(manager.allocated_bytes(), 512);
6021        assert_eq!(
6022            manager.peak_bytes(),
6023            512,
6024            "check-only refusal must not raise peak"
6025        );
6026        drop(baseline);
6027    }
6028
6029    #[test]
6030    fn memory_pressure_check_budget_reports_u64_representability_overflow() {
6031        let Some(device) = try_device() else {
6032            return;
6033        };
6034        let manager = Arc::new(GpuMemoryManager::new(
6035            device,
6036            MemoryBudget::with_limit(u64::MAX),
6037        ));
6038        manager
6039            .accounting
6040            .budget_reserved
6041            .store(u64::MAX - 3, Ordering::SeqCst);
6042        manager
6043            .accounting
6044            .allocated
6045            .store(u64::MAX - 3, Ordering::SeqCst);
6046        manager
6047            .accounting
6048            .peak
6049            .store(u64::MAX - 3, Ordering::SeqCst);
6050
6051        let error = manager
6052            .check_budget(8)
6053            .expect_err("the exact required byte count is not representable as u64");
6054
6055        assert_memory_pressure(
6056            error,
6057            "GPU memory pressure: layer=manager_check_budget current_bytes=18446744073709551612 requested_bytes=8 required_bytes=18446744073709551620 required_u64_overflow=true budget_bytes=18446744073709551615 prior_peak_bytes=18446744073709551612",
6058            u64::MAX,
6059            u64::MAX,
6060        );
6061        assert_eq!(manager.allocated_bytes(), u64::MAX - 3);
6062        assert_eq!(manager.peak_bytes(), u64::MAX - 3);
6063
6064        // Restore the synthetic accounting state before dropping the fixture.
6065        manager
6066            .accounting
6067            .budget_reserved
6068            .store(0, Ordering::SeqCst);
6069        manager.accounting.allocated.store(0, Ordering::SeqCst);
6070        manager.accounting.peak.store(0, Ordering::SeqCst);
6071    }
6072
6073    #[test]
6074    fn test_cuda_buffer_from_columns() {
6075        let Some(device) = try_device() else {
6076            return;
6077        };
6078        let budget = MemoryBudget::with_limit(1024 * 1024);
6079        let manager = Arc::new(GpuMemoryManager::new(device, budget));
6080
6081        let schema = Schema::new(vec![
6082            ("col1".to_string(), ScalarType::U32),
6083            ("col2".to_string(), ScalarType::U32),
6084        ]);
6085
6086        // Allocate columns (100 rows * 4 bytes = 400 bytes each)
6087        let col1 = manager.alloc::<u8>(400).expect("Alloc col1");
6088        let col2 = manager.alloc::<u8>(400).expect("Alloc col2");
6089
6090        let mut d_num_rows = manager.alloc::<u32>(1).expect("Alloc row count");
6091        manager
6092            .device()
6093            .inner()
6094            .htod_sync_copy_into(&[100u32], &mut d_num_rows)
6095            .expect("Upload row count");
6096        let buffer =
6097            CudaBuffer::from_columns(vec![col1.into(), col2.into()], 100, d_num_rows, schema);
6098
6099        assert_eq!(buffer.num_rows(), 100);
6100        assert_eq!(buffer.arity(), 2);
6101        assert!(!buffer.is_empty());
6102        assert!(buffer.column(0).is_some());
6103        assert!(buffer.column(1).is_some());
6104        assert!(buffer.column(2).is_none());
6105    }
6106
6107    #[test]
6108    fn test_cuda_buffer_from_columns_mismatch() {
6109        let schema = Schema::new(vec![
6110            ("col1".to_string(), ScalarType::U32),
6111            ("col2".to_string(), ScalarType::U32),
6112        ]);
6113
6114        let Some(device) = try_device() else {
6115            return;
6116        };
6117        let budget = MemoryBudget::with_limit(1024 * 1024);
6118        let manager = Arc::new(GpuMemoryManager::new(device, budget));
6119        let mut d_num_rows = manager.alloc::<u32>(1).expect("Alloc row count");
6120        manager
6121            .device()
6122            .inner()
6123            .htod_sync_copy_into(&[100u32], &mut d_num_rows)
6124            .expect("Upload row count");
6125
6126        // This should panic: 0 columns but schema has 2.
6127        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
6128            CudaBuffer::from_columns(vec![], 100, d_num_rows, schema);
6129        }));
6130        assert!(
6131            result.is_err(),
6132            "Expected from_columns to panic on schema mismatch"
6133        );
6134    }
6135
6136    fn try_runtime() -> Option<(
6137        Arc<CudaDevice>,
6138        Arc<crate::device_runtime::XlogDeviceRuntime>,
6139    )> {
6140        use crate::device_runtime::{
6141            AsyncCudaResource, DeviceMemoryResource, GlobalDeviceBudget, StreamPool,
6142            XlogDeviceRuntime,
6143        };
6144        let device = try_device()?;
6145        let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6146        let async_resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
6147            AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
6148        );
6149        let budget: Box<dyn DeviceMemoryResource + Send + Sync> =
6150            Box::new(GlobalDeviceBudget::new(async_resource, 64 * 1024 * 1024));
6151        Some((
6152            Arc::clone(&device),
6153            Arc::new(XlogDeviceRuntime::with_resource(
6154                Arc::clone(&device),
6155                0,
6156                pool,
6157                budget,
6158            )),
6159        ))
6160    }
6161
6162    fn try_unbudgeted_runtime() -> Option<(
6163        Arc<CudaDevice>,
6164        Arc<crate::device_runtime::XlogDeviceRuntime>,
6165    )> {
6166        use crate::device_runtime::{
6167            AsyncCudaResource, DeviceMemoryResource, StreamPool, XlogDeviceRuntime,
6168        };
6169        let device = try_device()?;
6170        let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6171        let resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
6172            AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
6173        );
6174        Some((
6175            Arc::clone(&device),
6176            Arc::new(XlogDeviceRuntime::with_resource(
6177                Arc::clone(&device),
6178                0,
6179                pool,
6180                resource,
6181            )),
6182        ))
6183    }
6184
6185    fn try_runtime_with_deallocation_failure() -> Option<(
6186        Arc<CudaDevice>,
6187        Arc<crate::device_runtime::XlogDeviceRuntime>,
6188        Arc<AtomicU64>,
6189    )> {
6190        use crate::device_runtime::{StreamPool, XlogDeviceRuntime};
6191        let device = try_device()?;
6192        let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6193        let deallocate_calls = Arc::new(AtomicU64::new(0));
6194        let resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6195            Box::new(FailAfterDeallocateResource {
6196                inner: DirectCudaResource::new(Arc::clone(&device), 0),
6197                deallocate_calls: Arc::clone(&deallocate_calls),
6198            });
6199        Some((
6200            Arc::clone(&device),
6201            Arc::new(XlogDeviceRuntime::with_resource(
6202                Arc::clone(&device),
6203                0,
6204                pool,
6205                resource,
6206            )),
6207            deallocate_calls,
6208        ))
6209    }
6210
6211    fn try_runtime_with_first_allocation_failure(
6212        limit: usize,
6213    ) -> Option<(
6214        Arc<CudaDevice>,
6215        Arc<crate::device_runtime::XlogDeviceRuntime>,
6216    )> {
6217        use crate::device_runtime::{GlobalDeviceBudget, StreamPool, XlogDeviceRuntime};
6218        let device = try_device()?;
6219        let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6220        let failing_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6221            Box::new(FailFirstAllocationResource {
6222                inner: DirectCudaResource::new(Arc::clone(&device), 0),
6223                fail_next: std::sync::atomic::AtomicBool::new(true),
6224            });
6225        let budget_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6226            Box::new(GlobalDeviceBudget::new(failing_resource, limit));
6227        Some((
6228            Arc::clone(&device),
6229            Arc::new(XlogDeviceRuntime::with_resource(
6230                Arc::clone(&device),
6231                0,
6232                pool,
6233                budget_resource,
6234            )),
6235        ))
6236    }
6237
6238    fn try_runtime_with_budget(
6239        limit: usize,
6240    ) -> Option<(
6241        Arc<CudaDevice>,
6242        Arc<crate::device_runtime::XlogDeviceRuntime>,
6243    )> {
6244        use crate::device_runtime::{
6245            DeviceMemoryResource, DirectCudaResource, GlobalDeviceBudget, StreamPool,
6246            XlogDeviceRuntime,
6247        };
6248        let device = try_device()?;
6249        let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6250        let direct_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6251            Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
6252        let budget_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6253            Box::new(GlobalDeviceBudget::new(direct_resource, limit));
6254        Some((
6255            Arc::clone(&device),
6256            Arc::new(XlogDeviceRuntime::with_resource(
6257                Arc::clone(&device),
6258                0,
6259                pool,
6260                budget_resource,
6261            )),
6262        ))
6263    }
6264
6265    fn try_runtime_with_logging_budget(
6266        limit: usize,
6267    ) -> Option<(
6268        Arc<CudaDevice>,
6269        Arc<crate::device_runtime::XlogDeviceRuntime>,
6270        Arc<crate::device_runtime::InMemorySink>,
6271    )> {
6272        use crate::device_runtime::{
6273            DeviceMemoryResource, DirectCudaResource, GlobalDeviceBudget, InMemorySink,
6274            LoggingResource, LoggingSink, StreamPool, XlogDeviceRuntime,
6275        };
6276        let device = try_device()?;
6277        let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
6278        let direct_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6279            Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
6280        let budget_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6281            Box::new(GlobalDeviceBudget::new(direct_resource, limit));
6282        let sink = Arc::new(InMemorySink::new());
6283        let logging_sink: Arc<dyn LoggingSink> = sink.clone();
6284        let logging_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
6285            Box::new(LoggingResource::new(budget_resource, logging_sink));
6286        Some((
6287            Arc::clone(&device),
6288            Arc::new(XlogDeviceRuntime::with_resource(
6289                Arc::clone(&device),
6290                0,
6291                pool,
6292                logging_resource,
6293            )),
6294            sink,
6295        ))
6296    }
6297
6298    fn pause_request_after_local_reservation(
6299        manager: &GpuMemoryManager,
6300        paused_bytes: u64,
6301    ) -> (Arc<std::sync::Barrier>, Arc<std::sync::Barrier>) {
6302        let entered = Arc::new(std::sync::Barrier::new(2));
6303        let release = Arc::new(std::sync::Barrier::new(2));
6304        let hook_entered = Arc::clone(&entered);
6305        let hook_release = Arc::clone(&release);
6306        *manager
6307            .after_local_reservation_hook
6308            .lock()
6309            .expect("after-local-reservation test hook poisoned") = Some(Arc::new(move |bytes| {
6310            if bytes == paused_bytes {
6311                hook_entered.wait();
6312                hook_release.wait();
6313            }
6314        }));
6315        (entered, release)
6316    }
6317
6318    #[test]
6319    fn memory_pressure_concurrent_alloc_raw_refusal_excludes_provisional_bytes() {
6320        let Some((device, runtime)) = try_runtime_with_budget(4096) else {
6321            return;
6322        };
6323        let manager = Arc::new(GpuMemoryManager::with_runtime(
6324            device,
6325            MemoryBudget::with_limit(8192),
6326            runtime,
6327        ));
6328        let (paused, release) = pause_request_after_local_reservation(&manager, 4096);
6329
6330        let refused_manager = Arc::clone(&manager);
6331        let refused = std::thread::spawn(move || {
6332            refused_manager
6333                .alloc_raw(4096, AllocTag::UNTAGGED)
6334                .expect_err("runtime must refuse the paused request")
6335        });
6336        paused.wait();
6337
6338        let admitted = manager
6339            .alloc_raw(1024, AllocTag::UNTAGGED)
6340            .expect("concurrent smaller request must be admitted");
6341        release.wait();
6342        let error = refused.join().expect("paused allocation thread panicked");
6343
6344        assert_memory_pressure(
6345            error,
6346            "GPU memory pressure: layer=device_runtime current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
6347            5120,
6348            4096,
6349        );
6350        assert_eq!(manager.allocated_bytes(), 1024);
6351        assert_eq!(
6352            manager.peak_bytes(),
6353            1024,
6354            "refused provisional bytes must never enter the admitted peak"
6355        );
6356        drop(admitted);
6357    }
6358
6359    #[test]
6360    fn memory_pressure_concurrent_typed_alloc_refusal_excludes_provisional_bytes() {
6361        let Some((device, runtime)) = try_runtime_with_budget(4096) else {
6362            return;
6363        };
6364        let manager = Arc::new(GpuMemoryManager::with_runtime(
6365            device,
6366            MemoryBudget::with_limit(8192),
6367            runtime,
6368        ));
6369        let (paused, release) = pause_request_after_local_reservation(&manager, 4096);
6370
6371        let refused_manager = Arc::clone(&manager);
6372        let refused = std::thread::spawn(move || match refused_manager.alloc::<u8>(4096) {
6373            Err(error) => error,
6374            Ok(_) => panic!("runtime must refuse the paused typed request"),
6375        });
6376        paused.wait();
6377
6378        let admitted = manager
6379            .alloc::<u8>(1024)
6380            .expect("concurrent smaller typed request must be admitted");
6381        release.wait();
6382        let error = refused.join().expect("paused allocation thread panicked");
6383
6384        assert_memory_pressure(
6385            error,
6386            "GPU memory pressure: layer=device_runtime current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
6387            5120,
6388            4096,
6389        );
6390        assert_eq!(manager.allocated_bytes(), 1024);
6391        assert_eq!(
6392            manager.peak_bytes(),
6393            1024,
6394            "refused provisional bytes must never enter the admitted peak"
6395        );
6396        drop(admitted);
6397    }
6398
6399    #[test]
6400    fn memory_pressure_alloc_raw_reports_exact_local_pressure() {
6401        let Some((device, runtime)) = try_runtime_with_budget(64 * 1024) else {
6402            return;
6403        };
6404        let manager = Arc::new(GpuMemoryManager::with_runtime(
6405            device,
6406            MemoryBudget::with_limit(4096),
6407            runtime,
6408        ));
6409        let baseline = manager
6410            .alloc_raw(1024, AllocTag::UNTAGGED)
6411            .expect("baseline allocation");
6412
6413        let error = manager
6414            .alloc_raw(4096, AllocTag::UNTAGGED)
6415            .expect_err("cumulative allocation must exceed the local budget");
6416
6417        assert_memory_pressure(
6418            error,
6419            "GPU memory pressure: layer=manager_alloc_raw current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
6420            5120,
6421            4096,
6422        );
6423        assert_eq!(manager.allocated_bytes(), 1024);
6424        assert_eq!(manager.peak_bytes(), 1024, "refusal must not raise peak");
6425        drop(baseline);
6426    }
6427
6428    #[test]
6429    fn memory_pressure_alloc_raw_drop_restores_local_headroom() {
6430        let Some((device, runtime)) = try_runtime_with_budget(64 * 1024) else {
6431            return;
6432        };
6433        let manager = Arc::new(GpuMemoryManager::with_runtime(
6434            device,
6435            MemoryBudget::with_limit(1024),
6436            runtime,
6437        ));
6438
6439        let allocation = manager
6440            .alloc_raw(1024, AllocTag::UNTAGGED)
6441            .expect("initial allocation must consume the local budget");
6442        assert_eq!(manager.allocated_bytes(), 1024);
6443        assert_eq!(manager.remaining_bytes(), 0);
6444
6445        drop(allocation);
6446        assert_eq!(manager.allocated_bytes(), 0);
6447        assert_eq!(
6448            manager.remaining_bytes(),
6449            1024,
6450            "dropping an admitted raw allocation must restore local headroom"
6451        );
6452
6453        let replacement = manager
6454            .alloc_raw(1024, AllocTag::UNTAGGED)
6455            .expect("restored local headroom must admit a replacement allocation");
6456        drop(replacement);
6457    }
6458
6459    #[test]
6460    fn memory_pressure_runtime_rejection_preserves_peak() {
6461        let Some((device, runtime)) = try_runtime_with_budget(4096) else {
6462            return;
6463        };
6464        let manager = Arc::new(GpuMemoryManager::with_runtime(
6465            device,
6466            MemoryBudget::with_limit(8192),
6467            runtime,
6468        ));
6469        let baseline = manager
6470            .alloc_raw(1024, AllocTag::UNTAGGED)
6471            .expect("baseline allocation");
6472
6473        let error = manager
6474            .alloc_raw(4096, AllocTag::UNTAGGED)
6475            .expect_err("runtime budget must reject the cumulative allocation");
6476
6477        assert_memory_pressure(
6478            error,
6479            "GPU memory pressure: layer=device_runtime current_bytes=1024 requested_bytes=4096 required_bytes=5120 required_u64_overflow=false budget_bytes=4096 prior_peak_bytes=1024",
6480            5120,
6481            4096,
6482        );
6483        assert_eq!(
6484            manager.allocated_bytes(),
6485            1024,
6486            "local reservation must roll back"
6487        );
6488        assert_eq!(
6489            manager.peak_bytes(),
6490            1024,
6491            "runtime refusal must not advance the manager peak"
6492        );
6493        drop(baseline);
6494    }
6495
6496    /// xlog-owned DLPack column constructed from a
6497    /// runtime-backed slice exposes its `DeviceBlock` via
6498    /// `runtime_block()` and reports `is_external() == false`.
6499    /// The recorder will record it normally instead of
6500    /// strict-rejecting.
6501    #[test]
6502    fn test_xlog_owned_dlpack_runtime_backed_carries_identity() {
6503        let Some((device, runtime)) = try_runtime() else {
6504            return;
6505        };
6506        let manager = Arc::new(GpuMemoryManager::with_runtime(
6507            Arc::clone(&device),
6508            MemoryBudget::with_limit(1024 * 1024),
6509            Arc::clone(&runtime),
6510        ));
6511        let slice = manager.alloc::<u8>(64).expect("alloc runtime-backed");
6512        assert!(slice.runtime_block().is_some());
6513        let stream = device.inner().stream().clone();
6514        let slice = Arc::new(slice);
6515        let tensor = crate::dlpack::export_slice_managed_tensor(
6516            Arc::clone(&slice),
6517            device.ordinal() as i32,
6518            crate::dlpack::DLDataType {
6519                code: crate::dlpack::K_DLUINT,
6520                bits: 8,
6521                lanes: 1,
6522            },
6523            1,
6524            slice.len(),
6525        )
6526        .expect("real export owner");
6527        stream.synchronize().expect("producer ready");
6528        let col = CudaColumn::dlpack_xlog_owned(slice, stream, tensor);
6529        assert!(
6530            !col.is_external(),
6531            "xlog-owned DLPack column must report is_external=false"
6532        );
6533        assert!(
6534            col.runtime_block().is_some(),
6535            "xlog-owned DLPack column over a runtime-backed slice must expose runtime_block"
6536        );
6537    }
6538
6539    /// xlog-owned DLPack over a LEGACY (cudarc-backed) slice:
6540    /// `is_external()` is still false (xlog owns the
6541    /// allocation), but `runtime_block()` is None because the
6542    /// underlying slice has no runtime block. Strict recorders
6543    /// will reject with the "legacy cudarc-backed" message
6544    /// rather than the "external memory" message.
6545    #[test]
6546    fn test_xlog_owned_dlpack_legacy_backed_no_runtime_block() {
6547        let Some(device) = try_device() else {
6548            return;
6549        };
6550        let manager = Arc::new(GpuMemoryManager::new(
6551            Arc::clone(&device),
6552            MemoryBudget::with_limit(1024 * 1024),
6553        ));
6554        let slice = manager.alloc::<u8>(64).expect("alloc legacy");
6555        assert!(slice.runtime_block().is_none());
6556        let stream = device.inner().stream().clone();
6557        let slice = Arc::new(slice);
6558        let tensor = crate::dlpack::export_slice_managed_tensor(
6559            Arc::clone(&slice),
6560            device.ordinal() as i32,
6561            crate::dlpack::DLDataType {
6562                code: crate::dlpack::K_DLUINT,
6563                bits: 8,
6564                lanes: 1,
6565            },
6566            1,
6567            slice.len(),
6568        )
6569        .expect("real export owner");
6570        stream.synchronize().expect("producer ready");
6571        let col = CudaColumn::dlpack_xlog_owned(slice, stream, tensor);
6572        assert!(
6573            !col.is_external(),
6574            "xlog-owned DLPack column is owned regardless of allocator backing"
6575        );
6576        assert!(
6577            col.runtime_block().is_none(),
6578            "legacy-backed slice has no runtime block, even when wrapped xlog-owned"
6579        );
6580    }
6581
6582    /// True external DLPack (no source_slice) — the existing
6583    /// `dlpack` constructor — keeps reporting `is_external=true`
6584    /// and `runtime_block=None`. Strict recorders reject with
6585    /// the "external memory" message.
6586    #[test]
6587    fn test_external_dlpack_remains_external() {
6588        let Some(device) = try_device() else {
6589            return;
6590        };
6591        let stream = device.inner().stream().clone();
6592        let manager = Arc::new(GpuMemoryManager::new(
6593            Arc::clone(&device),
6594            MemoryBudget::with_limit(1024 * 1024),
6595        ));
6596        let slice = Arc::new(manager.alloc::<u8>(1).expect("external allocation"));
6597        let ptr = slice.device_ptr_value();
6598        let tensor = crate::dlpack::export_slice_managed_tensor(
6599            slice,
6600            device.ordinal() as i32,
6601            crate::dlpack::DLDataType {
6602                code: crate::dlpack::K_DLUINT,
6603                bits: 8,
6604                lanes: 1,
6605            },
6606            1,
6607            1,
6608        )
6609        .expect("real external owner");
6610        stream.synchronize().expect("producer ready");
6611        // SAFETY: the token retains the live one-byte allocation on this context,
6612        // producer work has completed, and there are no foreign accesses.
6613        let col = unsafe { CudaColumn::dlpack(ptr, 1, stream, tensor) };
6614        assert!(
6615            col.is_external(),
6616            "true external DLPack column must report is_external=true"
6617        );
6618        assert!(
6619            col.runtime_block().is_none(),
6620            "true external DLPack column has no xlog-side runtime block"
6621        );
6622    }
6623
6624    /// xlog-owned Arrow device column carries identity through
6625    /// `arrow_device_xlog_owned`. Mirrors the DLPack test;
6626    /// builds a minimal `ArrowDeviceImport` from an empty
6627    /// `ArrayData`.
6628    #[test]
6629    fn test_xlog_owned_arrow_device_runtime_backed_carries_identity() {
6630        let Some((device, runtime)) = try_runtime() else {
6631            return;
6632        };
6633        let manager = Arc::new(GpuMemoryManager::with_runtime(
6634            Arc::clone(&device),
6635            MemoryBudget::with_limit(1024 * 1024),
6636            Arc::clone(&runtime),
6637        ));
6638        let slice = manager.alloc::<u8>(64).expect("alloc runtime-backed");
6639        assert!(slice.runtime_block().is_some());
6640        let stream = device.inner().stream().clone();
6641        // Synthesize a minimal ArrowDeviceImport via empty
6642        // ArrayData; Arrow is not exercised on the data path
6643        // here — the recorder only reads the column metadata.
6644        let import = Arc::new(crate::arrow_device::ArrowDeviceImport::new(
6645            arrow::array::ArrayData::new_null(&arrow::datatypes::DataType::UInt8, 0),
6646        ));
6647        let col = CudaColumn::arrow_device_xlog_owned(Arc::new(slice), stream, import);
6648        assert!(
6649            !col.is_external(),
6650            "xlog-owned Arrow device column must report is_external=false"
6651        );
6652        assert!(
6653            col.runtime_block().is_some(),
6654            "xlog-owned Arrow column over a runtime-backed slice must expose runtime_block"
6655        );
6656    }
6657
6658    /// True external Arrow device column (no source_slice)
6659    /// keeps reporting external + no runtime block.
6660    #[test]
6661    fn test_external_arrow_device_remains_external() {
6662        let Some(device) = try_device() else {
6663            return;
6664        };
6665        let stream = device.inner().stream().clone();
6666        let import = Arc::new(crate::arrow_device::ArrowDeviceImport::new(
6667            arrow::array::ArrayData::new_null(&arrow::datatypes::DataType::UInt8, 0),
6668        ));
6669        // SAFETY: the empty ArrayData owns this empty span; no bytes or producer
6670        // work exist, and its release is safe on any host thread.
6671        let col = unsafe { CudaColumn::arrow_device(0, 0, stream, import) };
6672        assert!(
6673            col.is_external(),
6674            "true external Arrow column must report is_external=true"
6675        );
6676        assert!(
6677            col.runtime_block().is_none(),
6678            "true external Arrow column has no xlog-side runtime block"
6679        );
6680    }
6681}