1use std::mem::ManuallyDrop;
7use std::ops::{Deref, DerefMut};
8use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
9use std::sync::Arc;
10
11use cudarc::driver::{CudaSlice, CudaStream, DevicePtr, DevicePtrMut, DeviceSlice, SyncOnDrop};
12use xlog_core::{MemoryBudget, Result, Schema, XlogError};
13
14use crate::arrow_device::ArrowDeviceImport;
15use crate::cuda_compat::{AsKernelParam, DeviceParamStorage, IntoKernelParamStorage};
16use crate::device_runtime::{
17 AllocTag, BlockId, BlockState, DeviceBlock, ResourceError, RuntimeMemoryReservation, StreamId,
18 XlogDeviceRuntime,
19};
20use crate::dlpack::DlpackManagedTensor;
21use crate::CudaDevice;
22
23#[cfg(test)]
24type AfterLocalReservationHook = std::sync::Mutex<Option<Arc<dyn Fn(u64) + Send + Sync + 'static>>>;
25
26pub struct GpuMemoryManager {
58 device: Arc<CudaDevice>,
60 budget: MemoryBudget,
62 accounting: Arc<GpuMemoryAccounting>,
64 runtime: Option<Arc<XlogDeviceRuntime>>,
69 #[cfg(test)]
72 after_local_reservation_hook: AfterLocalReservationHook,
73}
74
75#[derive(Default)]
76struct GpuMemoryAccounting {
77 mutation_lock: std::sync::Mutex<()>,
80 budget_reserved: AtomicU64,
85 allocated: AtomicU64,
89 peak: AtomicU64,
93 alloc_count: AtomicU64,
98 deallocation_failure_count: AtomicU64,
101 deallocation_failure_bytes: AtomicU64,
102}
103
104#[must_use = "dropping the reservation immediately releases its unused budget"]
110pub struct GpuMemoryReservation {
111 manager: Arc<GpuMemoryManager>,
112 runtime_reservation: Option<RuntimeMemoryReservation>,
113 total_bytes: u64,
114 remaining_bytes: u64,
115}
116
117impl std::fmt::Debug for GpuMemoryReservation {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_struct("GpuMemoryReservation")
120 .field("total_bytes", &self.total_bytes)
121 .field("remaining_bytes", &self.remaining_bytes)
122 .finish()
123 }
124}
125
126impl GpuMemoryReservation {
127 pub fn memory_manager_ptr_value(&self) -> usize {
129 Arc::as_ptr(&self.manager) as usize
130 }
131
132 pub fn total_bytes(&self) -> u64 {
134 self.total_bytes
135 }
136
137 pub fn remaining_bytes(&self) -> u64 {
139 self.remaining_bytes
140 }
141
142 pub fn used_bytes(&self) -> u64 {
144 self.total_bytes - self.remaining_bytes
145 }
146
147 pub fn alloc<T: cudarc::driver::DeviceRepr>(
149 &mut self,
150 len: usize,
151 ) -> Result<TrackedCudaSlice<T>> {
152 self.manager
153 .accounting
154 .alloc_count
155 .fetch_add(1, Ordering::Relaxed);
156 let bytes = (len as u64)
157 .checked_mul(std::mem::size_of::<T>() as u64)
158 .ok_or_else(|| XlogError::Kernel("Allocation size overflow".to_string()))?;
159 let used = self.used_bytes();
160 if bytes > self.remaining_bytes {
161 return Err(MemoryPressure {
162 layer: "manager_reservation_alloc",
163 current_bytes: used as u128,
164 requested_bytes: bytes as u128,
165 budget_bytes: self.total_bytes,
166 prior_peak_bytes: self.manager.accounting.peak.load(Ordering::SeqCst),
167 }
168 .into_error());
169 }
170
171 self.remaining_bytes -= bytes;
172 let manager = Arc::clone(&self.manager);
173 let runtime_reservation = self.runtime_reservation.as_mut();
174 match manager.alloc_after_local_reservation::<T>(len, bytes, runtime_reservation) {
175 Ok(allocation) => Ok(allocation),
176 Err(error) => {
177 self.remaining_bytes = self
178 .remaining_bytes
179 .checked_add(bytes)
180 .expect("reservation rollback overflow");
181 Err(error)
182 }
183 }
184 }
185
186 pub fn alloc_raw(&mut self, bytes: usize, tag: AllocTag) -> Result<RuntimeAllocBlock> {
189 let bytes_u64 = u64::try_from(bytes)
190 .map_err(|_| XlogError::Kernel("Allocation size overflow".to_string()))?;
191 let used = self.used_bytes();
192 if bytes_u64 > self.remaining_bytes {
193 return Err(MemoryPressure {
194 layer: "manager_reservation_alloc_raw",
195 current_bytes: used as u128,
196 requested_bytes: bytes_u64 as u128,
197 budget_bytes: self.total_bytes,
198 prior_peak_bytes: self.manager.accounting.peak.load(Ordering::SeqCst),
199 }
200 .into_error());
201 }
202
203 self.remaining_bytes -= bytes_u64;
204 let manager = Arc::clone(&self.manager);
205 let runtime_reservation = self.runtime_reservation.as_mut();
206 match manager.alloc_raw_after_local_reservation(bytes, bytes_u64, tag, runtime_reservation)
207 {
208 Ok(allocation) => Ok(allocation),
209 Err(error) => {
210 self.remaining_bytes = self
211 .remaining_bytes
212 .checked_add(bytes_u64)
213 .expect("reservation rollback overflow");
214 Err(error)
215 }
216 }
217 }
218}
219
220impl Drop for GpuMemoryReservation {
221 fn drop(&mut self) {
222 let unused = std::mem::take(&mut self.remaining_bytes);
223 let release = self.manager.rollback_local_reservation(unused);
224 debug_assert!(release.is_ok(), "reservation release must be balanced");
225 }
226}
227
228struct MemoryPressure {
229 layer: &'static str,
230 current_bytes: u128,
231 requested_bytes: u128,
232 budget_bytes: u64,
233 prior_peak_bytes: u64,
234}
235
236impl MemoryPressure {
237 fn required_bytes(&self) -> u128 {
238 self.current_bytes + self.requested_bytes
239 }
240
241 fn into_error(self) -> XlogError {
242 let required_bytes = self.required_bytes();
243 let required_u64_overflow = required_bytes > u64::MAX as u128;
244 XlogError::ResourceExhausted {
245 context: format!(
246 "GPU memory pressure: layer={} current_bytes={} requested_bytes={} required_bytes={} required_u64_overflow={} budget_bytes={} prior_peak_bytes={}",
247 self.layer,
248 self.current_bytes,
249 self.requested_bytes,
250 required_bytes,
251 required_u64_overflow,
252 self.budget_bytes,
253 self.prior_peak_bytes,
254 ),
255 estimated_bytes: u64::try_from(required_bytes).unwrap_or(u64::MAX),
256 budget_bytes: self.budget_bytes,
257 }
258 }
259}
260
261enum Backing {
266 Cudarc,
272 Runtime {
282 runtime: Arc<XlogDeviceRuntime>,
283 block: Option<DeviceBlock>,
284 },
285}
286
287fn poison_free_enabled() -> bool {
291 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
292 *ENABLED.get_or_init(|| std::env::var("XLOG_DEBUG_POISON_FREE").map(|v| v == "1") == Ok(true))
293}
294
295fn poison_alloc_enabled() -> bool {
299 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
300 *ENABLED.get_or_init(|| std::env::var("XLOG_DEBUG_POISON_ALLOC").map(|v| v == "1") == Ok(true))
301}
302
303fn alloc_guard() -> Option<&'static std::sync::Mutex<std::collections::BTreeMap<u64, u64>>> {
308 static GUARD: std::sync::OnceLock<
309 Option<std::sync::Mutex<std::collections::BTreeMap<u64, u64>>>,
310 > = std::sync::OnceLock::new();
311 GUARD
312 .get_or_init(|| {
313 if std::env::var("XLOG_DEBUG_ALLOC_GUARD").map(|v| v == "1") == Ok(true) {
314 Some(std::sync::Mutex::new(std::collections::BTreeMap::new()))
315 } else {
316 None
317 }
318 })
319 .as_ref()
320}
321
322fn alloc_guard_insert(ptr: u64, bytes: u64) {
323 let Some(guard) = alloc_guard() else { return };
324 if bytes == 0 {
325 return;
326 }
327 let mut live = guard.lock().unwrap();
328 if let Some((&p, &b)) = live.range(..=ptr).next_back() {
331 if p + b > ptr {
332 panic!(
333 "ALLOC GUARD: new allocation [{:#x}, {:#x}) overlaps live [{:#x}, {:#x})",
334 ptr,
335 ptr + bytes,
336 p,
337 p + b
338 );
339 }
340 }
341 if let Some((&p, _)) = live.range(ptr + 1..).next() {
342 if ptr + bytes > p {
343 panic!(
344 "ALLOC GUARD: new allocation [{:#x}, {:#x}) overlaps live starting at {:#x}",
345 ptr,
346 ptr + bytes,
347 p
348 );
349 }
350 }
351 live.insert(ptr, bytes);
352}
353
354fn alloc_guard_remove(ptr: u64) {
355 let Some(guard) = alloc_guard() else { return };
356 guard.lock().unwrap().remove(&ptr);
357}
358
359pub struct TrackedCudaSlice<T: cudarc::driver::DeviceRepr> {
365 bytes: u64,
366 manager: Arc<GpuMemoryManager>,
367 inner: ManuallyDrop<CudaSlice<T>>,
368 raw_ptr: cudarc::driver::sys::CUdeviceptr,
369 backing: Backing,
370}
371
372#[derive(Clone)]
373pub(crate) struct RuntimeAllocationIdentity {
374 pub(crate) manager_id: usize,
375 pub(crate) allocation_ptr: u64,
376 pub(crate) allocation_bytes: usize,
377 pub(crate) block_id: BlockId,
378 pub(crate) block_bytes: usize,
379 pub(crate) block_state: BlockState,
380 pub(crate) context: Arc<cudarc::driver::CudaContext>,
381}
382
383impl<T: cudarc::driver::DeviceRepr> Deref for TrackedCudaSlice<T> {
384 type Target = CudaSlice<T>;
385
386 fn deref(&self) -> &Self::Target {
387 &self.inner
388 }
389}
390
391impl<T: cudarc::driver::DeviceRepr> DerefMut for TrackedCudaSlice<T> {
392 fn deref_mut(&mut self) -> &mut Self::Target {
393 &mut self.inner
394 }
395}
396
397impl<T: cudarc::driver::DeviceRepr> DeviceSlice<T> for TrackedCudaSlice<T> {
398 fn len(&self) -> usize {
399 self.inner.len()
400 }
401
402 fn stream(&self) -> &Arc<CudaStream> {
403 self.inner.stream()
404 }
405}
406
407impl<T: cudarc::driver::DeviceRepr> DevicePtr<T> for TrackedCudaSlice<T> {
408 fn device_ptr<'a>(
409 &'a self,
410 stream: &'a CudaStream,
411 ) -> (cudarc::driver::sys::CUdeviceptr, SyncOnDrop<'a>) {
412 DevicePtr::device_ptr(&*self.inner, stream)
415 }
416}
417
418impl<T: cudarc::driver::DeviceRepr> DevicePtrMut<T> for TrackedCudaSlice<T> {
419 fn device_ptr_mut<'a>(
420 &'a mut self,
421 stream: &'a CudaStream,
422 ) -> (cudarc::driver::sys::CUdeviceptr, SyncOnDrop<'a>) {
423 DevicePtrMut::device_ptr_mut(&mut *self.inner, stream)
424 }
425}
426
427impl<T: cudarc::driver::DeviceRepr> TrackedCudaSlice<T> {
428 pub fn device_ptr(&self) -> &cudarc::driver::sys::CUdeviceptr {
429 &self.raw_ptr
430 }
431
432 pub fn device_ptr_value(&self) -> cudarc::driver::sys::CUdeviceptr {
433 self.raw_ptr
434 }
435
436 pub fn memory_manager_ptr_value(&self) -> usize {
438 Arc::as_ptr(&self.manager) as usize
439 }
440
441 pub(crate) fn runtime_allocation_identity(&self) -> Result<Option<RuntimeAllocationIdentity>> {
442 let Some(block) = self.runtime_block() else {
443 return Ok(None);
444 };
445 let allocation_bytes = self
446 .len()
447 .checked_mul(std::mem::size_of::<T>())
448 .ok_or_else(|| XlogError::Kernel("runtime allocation byte size overflow".into()))?;
449 Ok(Some(RuntimeAllocationIdentity {
450 manager_id: self.memory_manager_ptr_value(),
451 allocation_ptr: self.device_ptr_value(),
452 allocation_bytes,
453 block_id: BlockId::from_block(block),
454 block_bytes: block.bytes,
455 block_state: block.state,
456 context: Arc::clone(DeviceSlice::stream(self).context()),
457 }))
458 }
459
460 pub fn runtime_block(&self) -> Option<&crate::device_runtime::DeviceBlock> {
475 match &self.backing {
476 Backing::Cudarc => None,
477 Backing::Runtime { block, .. } => block.as_ref(),
478 }
479 }
480
481 pub fn into_bytes(self) -> TrackedCudaSlice<u8> {
490 let this = ManuallyDrop::new(self);
498 let bytes = this.bytes;
499 let manager = Arc::clone(&this.manager);
500 let ptr = this.raw_ptr;
501
502 let len_bytes: usize = bytes
503 .try_into()
504 .expect("TrackedCudaSlice byte size must fit into usize");
505
506 let backing: Backing = unsafe { std::ptr::read(&this.backing) };
512
513 let new_inner = unsafe {
524 manager
525 .device
526 .inner()
527 .upgrade_device_ptr::<u8>(ptr, len_bytes)
528 };
529
530 TrackedCudaSlice {
531 bytes,
532 manager,
533 inner: ManuallyDrop::new(new_inner),
534 raw_ptr: ptr,
535 backing,
536 }
537 }
538}
539
540impl<T: cudarc::driver::DeviceRepr> AsKernelParam for &TrackedCudaSlice<T> {
541 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
542 ((*self).device_ptr() as *const cudarc::driver::sys::CUdeviceptr)
543 .cast_mut()
544 .cast()
545 }
546}
547
548impl<T: cudarc::driver::DeviceRepr> AsKernelParam for &mut TrackedCudaSlice<T> {
549 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
550 ((self.device_ptr()) as *const cudarc::driver::sys::CUdeviceptr)
551 .cast_mut()
552 .cast()
553 }
554}
555
556impl<'a, T: cudarc::driver::DeviceRepr> IntoKernelParamStorage for &'a TrackedCudaSlice<T> {
557 type Storage = DeviceParamStorage<'a>;
558
559 fn into_kernel_param_storage(self) -> Self::Storage {
560 let (ptr, sync) = DevicePtr::device_ptr(&*self.inner, self.inner.stream());
561 DeviceParamStorage::synced(ptr, sync)
562 }
563}
564
565impl<T: cudarc::driver::DeviceRepr> IntoKernelParamStorage for &mut TrackedCudaSlice<T> {
566 type Storage = DeviceParamStorage<'static>;
567
568 fn into_kernel_param_storage(self) -> Self::Storage {
569 let stream = self.inner.stream().clone();
570 let (ptr, sync) = DevicePtrMut::device_ptr_mut(&mut *self.inner, &stream);
571 std::mem::forget(sync);
572 DeviceParamStorage::unsynced(ptr)
573 }
574}
575
576impl<T: cudarc::driver::DeviceRepr> Drop for TrackedCudaSlice<T> {
577 fn drop(&mut self) {
578 let released = match &mut self.backing {
579 Backing::Cudarc => {
580 if poison_free_enabled() && self.bytes > 0 {
586 unsafe {
587 let _ = cudarc::driver::sys::cuMemsetD8_v2(
588 self.raw_ptr,
589 0xDD,
590 self.bytes as usize,
591 );
592 }
593 }
594 alloc_guard_remove(self.raw_ptr);
595 unsafe { ManuallyDrop::drop(&mut self.inner) };
600 true
601 }
602 Backing::Runtime { runtime, block } => {
603 match block.take() {
609 Some(block) => match runtime.deallocate(block) {
610 Ok(()) => true,
611 Err(_) => {
612 self.manager.record_deallocation_failure(self.bytes);
613 false
614 }
615 },
616 None => false,
617 }
618 }
619 };
620 if released {
621 let release = self.manager.release_owned_allocation(self.bytes);
622 debug_assert!(release.is_ok(), "allocation release must be balanced");
623 }
624 }
625}
626
627impl GpuMemoryManager {
628 pub fn new(device: Arc<CudaDevice>, budget: MemoryBudget) -> Self {
634 Self {
635 device,
636 budget,
637 accounting: Arc::new(GpuMemoryAccounting::default()),
638 runtime: None,
639 #[cfg(test)]
640 after_local_reservation_hook: std::sync::Mutex::new(None),
641 }
642 }
643
644 pub fn with_runtime(
656 device: Arc<CudaDevice>,
657 budget: MemoryBudget,
658 runtime: Arc<XlogDeviceRuntime>,
659 ) -> Self {
660 Self {
661 device,
662 budget,
663 accounting: Arc::new(GpuMemoryAccounting::default()),
664 runtime: Some(runtime),
665 #[cfg(test)]
666 after_local_reservation_hook: std::sync::Mutex::new(None),
667 }
668 }
669
670 pub fn with_runtime_overlay(
678 self: &Arc<Self>,
679 runtime: Arc<XlogDeviceRuntime>,
680 ) -> Result<Arc<Self>> {
681 if !Arc::ptr_eq(&self.device, runtime.device()) {
682 return Err(XlogError::Kernel(
683 "GpuMemoryManager::with_runtime_overlay requires the runtime to share the exact CUDA device handle"
684 .to_string(),
685 ));
686 }
687 let device_ordinal = u32::try_from(self.device.ordinal()).map_err(|_| {
688 XlogError::Kernel(format!(
689 "CUDA device ordinal {} is not representable as u32",
690 self.device.ordinal()
691 ))
692 })?;
693 if runtime.device_ordinal() != device_ordinal {
694 return Err(XlogError::Kernel(format!(
695 "GpuMemoryManager::with_runtime_overlay device ordinal mismatch: manager={} runtime={}",
696 device_ordinal,
697 runtime.device_ordinal()
698 )));
699 }
700 if !runtime.supports_block_use_tracking() {
701 return Err(XlogError::Kernel(
702 "GpuMemoryManager::with_runtime_overlay requires a runtime with cross-stream block-use tracking"
703 .to_string(),
704 ));
705 }
706
707 Ok(Arc::new(Self {
708 device: Arc::clone(&self.device),
709 budget: self.budget.clone(),
710 accounting: Arc::clone(&self.accounting),
711 runtime: Some(runtime),
712 #[cfg(test)]
713 after_local_reservation_hook: std::sync::Mutex::new(None),
714 }))
715 }
716
717 pub fn reserve_bytes(self: &Arc<Self>, bytes: u64) -> Result<GpuMemoryReservation> {
726 let runtime_reservation = match &self.runtime {
727 Some(runtime) => {
728 let bytes_usize = usize::try_from(bytes).map_err(|_| {
729 XlogError::Kernel(format!(
730 "GPU reservation size {} bytes exceeds platform usize",
731 bytes
732 ))
733 })?;
734 Some(runtime.reserve_memory(bytes_usize).map_err(|error| {
735 map_resource_error(error, self.accounting.peak.load(Ordering::SeqCst))
736 })?)
737 }
738 None => None,
739 };
740 self.reserve_local_bytes(bytes, "manager_reserve")?;
741 Ok(GpuMemoryReservation {
742 manager: Arc::clone(self),
743 runtime_reservation,
744 total_bytes: bytes,
745 remaining_bytes: bytes,
746 })
747 }
748
749 fn reserve_local_bytes(&self, bytes: u64, layer: &'static str) -> Result<()> {
750 let _mutation = self
751 .accounting
752 .mutation_lock
753 .lock()
754 .expect("GPU memory accounting poisoned");
755 loop {
756 let current = self.accounting.budget_reserved.load(Ordering::SeqCst);
757 let required = current as u128 + bytes as u128;
758 if required > self.budget.device_bytes as u128 {
759 return Err(MemoryPressure {
760 layer,
761 current_bytes: current as u128,
762 requested_bytes: bytes as u128,
763 budget_bytes: self.budget.device_bytes,
764 prior_peak_bytes: self.accounting.peak.load(Ordering::SeqCst),
765 }
766 .into_error());
767 }
768 if self
769 .accounting
770 .budget_reserved
771 .compare_exchange(current, required as u64, Ordering::SeqCst, Ordering::SeqCst)
772 .is_ok()
773 {
774 return Ok(());
775 }
776 }
777 }
778
779 pub fn runtime(&self) -> Option<&Arc<XlogDeviceRuntime>> {
784 self.runtime.as_ref()
785 }
786
787 pub fn reap_pending_deallocations(&self) -> Result<()> {
793 let Some(runtime) = self.runtime.as_ref() else {
794 return Ok(());
795 };
796 runtime
797 .reap_pending()
798 .map_err(|error| map_resource_error(error, self.peak_bytes()))
799 }
800
801 fn rollback_local_reservation(&self, bytes: u64) -> Result<()> {
805 let _mutation = self
806 .accounting
807 .mutation_lock
808 .lock()
809 .expect("GPU memory accounting poisoned");
810 let previous = self.accounting.budget_reserved.load(Ordering::SeqCst);
811 let next = previous.checked_sub(bytes).ok_or_else(|| {
812 XlogError::Kernel(format!(
813 "GPU memory reservation release underflow: current_bytes={} requested_bytes={}",
814 previous, bytes
815 ))
816 })?;
817 self.accounting
818 .budget_reserved
819 .store(next, Ordering::SeqCst);
820 Ok(())
821 }
822
823 fn publish_admission(&self, bytes: u64) {
826 let _mutation = self
827 .accounting
828 .mutation_lock
829 .lock()
830 .expect("GPU memory accounting poisoned");
831 let previous = self.accounting.allocated.load(Ordering::SeqCst);
832 let admitted = previous
833 .checked_add(bytes)
834 .expect("admitted allocation accounting overflow");
835 self.accounting.allocated.store(admitted, Ordering::SeqCst);
836 self.accounting.peak.fetch_max(admitted, Ordering::SeqCst);
837 }
838
839 fn release_owned_allocation(&self, bytes: u64) -> Result<()> {
842 let _mutation = self
843 .accounting
844 .mutation_lock
845 .lock()
846 .expect("GPU memory accounting poisoned");
847 let admitted = self.accounting.allocated.load(Ordering::SeqCst);
848 let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
849 let next_admitted = admitted.checked_sub(bytes).ok_or_else(|| {
850 XlogError::Kernel(format!(
851 "GPU admitted allocation release underflow: current_bytes={} requested_bytes={}",
852 admitted, bytes
853 ))
854 })?;
855 let next_reserved = reserved.checked_sub(bytes).ok_or_else(|| {
856 XlogError::Kernel(format!(
857 "GPU local reservation release underflow: current_bytes={} requested_bytes={}",
858 reserved, bytes
859 ))
860 })?;
861 self.accounting
862 .allocated
863 .store(next_admitted, Ordering::SeqCst);
864 self.accounting
865 .budget_reserved
866 .store(next_reserved, Ordering::SeqCst);
867 Ok(())
868 }
869
870 fn record_deallocation_failure(&self, bytes: u64) {
871 let _ = self.accounting.deallocation_failure_count.fetch_update(
872 Ordering::SeqCst,
873 Ordering::SeqCst,
874 |current| Some(current.saturating_add(1)),
875 );
876 let _ = self.accounting.deallocation_failure_bytes.fetch_update(
877 Ordering::SeqCst,
878 Ordering::SeqCst,
879 |current| Some(current.saturating_add(bytes)),
880 );
881 }
882
883 pub fn alloc<T: cudarc::driver::DeviceRepr>(
906 self: &Arc<Self>,
907 len: usize,
908 ) -> Result<TrackedCudaSlice<T>> {
909 self.accounting.alloc_count.fetch_add(1, Ordering::Relaxed);
911
912 let bytes = (len as u64)
914 .checked_mul(std::mem::size_of::<T>() as u64)
915 .ok_or_else(|| XlogError::Kernel("Allocation size overflow".to_string()))?;
916
917 self.reserve_local_bytes(bytes, "manager_alloc")?;
918 match self.alloc_after_local_reservation::<T>(len, bytes, None) {
919 Ok(allocation) => Ok(allocation),
920 Err(error) => {
921 self.rollback_local_reservation(bytes)?;
922 Err(error)
923 }
924 }
925 }
926
927 fn alloc_after_local_reservation<T: cudarc::driver::DeviceRepr>(
928 self: &Arc<Self>,
929 len: usize,
930 bytes: u64,
931 runtime_reservation: Option<&mut RuntimeMemoryReservation>,
932 ) -> Result<TrackedCudaSlice<T>> {
933 #[cfg(test)]
934 self.run_after_local_reservation_hook(bytes);
935
936 if let Some(runtime) = &self.runtime {
937 if bytes == 0 {
956 let slice = unsafe {
957 self.device.inner().alloc::<T>(len).map_err(|e| {
958 XlogError::Kernel(format!("GPU allocation failed (zero-byte): {}", e))
959 })?
960 };
961 let (raw_ptr, sync) = DevicePtr::device_ptr(&slice, slice.stream());
962 std::mem::forget(sync);
963 self.publish_admission(bytes);
964 return Ok(TrackedCudaSlice {
965 bytes,
966 manager: Arc::clone(self),
967 inner: ManuallyDrop::new(slice),
968 raw_ptr,
969 backing: Backing::Cudarc,
970 });
971 }
972
973 let bytes_usize = match usize::try_from(bytes) {
983 Ok(v) => v,
984 Err(_) => {
985 return Err(XlogError::Kernel(format!(
986 "GPU allocation size {} bytes exceeds platform usize",
987 bytes
988 )));
989 }
990 };
991 let block_result = match runtime_reservation {
992 Some(reservation) => {
993 reservation.allocate(bytes_usize, StreamId::DEFAULT, AllocTag::UNTAGGED)
994 }
995 None => runtime.allocate(bytes_usize, StreamId::DEFAULT, AllocTag::UNTAGGED),
996 };
997 let block = match block_result {
998 Ok(b) => b,
999 Err(e) => {
1000 let prior_peak = self.accounting.peak.load(Ordering::SeqCst);
1001 return Err(map_resource_error(e, prior_peak));
1002 }
1003 };
1004 self.publish_admission(bytes);
1005 let raw_ptr = block.ptr;
1006 let typed_view = unsafe { self.device.inner().upgrade_device_ptr::<T>(raw_ptr, len) };
1014 return Ok(TrackedCudaSlice {
1015 bytes,
1016 manager: Arc::clone(self),
1017 inner: ManuallyDrop::new(typed_view),
1018 raw_ptr,
1019 backing: Backing::Runtime {
1020 runtime: Arc::clone(runtime),
1021 block: Some(block),
1022 },
1023 });
1024 }
1025
1026 let slice = unsafe {
1030 self.device
1031 .inner()
1032 .alloc::<T>(len)
1033 .map_err(|e| XlogError::Kernel(format!("GPU allocation failed: {}", e)))?
1034 };
1035 let (raw_ptr, sync) = DevicePtr::device_ptr(&slice, slice.stream());
1036 std::mem::forget(sync);
1037 alloc_guard_insert(raw_ptr, bytes);
1038 self.publish_admission(bytes);
1039
1040 if poison_alloc_enabled() && bytes > 0 {
1046 unsafe {
1047 let _ = cudarc::driver::sys::cuMemsetD8Async(
1048 raw_ptr,
1049 0xDD,
1050 bytes as usize,
1051 std::ptr::null_mut(),
1052 );
1053 }
1054 }
1055
1056 Ok(TrackedCudaSlice {
1057 bytes,
1058 manager: Arc::clone(self),
1059 inner: ManuallyDrop::new(slice),
1060 raw_ptr,
1061 backing: Backing::Cudarc,
1062 })
1063 }
1064
1065 pub fn check_budget(&self, bytes: u64) -> Result<()> {
1076 let current = self.accounting.budget_reserved.load(Ordering::SeqCst);
1079 let required = current as u128 + bytes as u128;
1080
1081 if required > self.budget.device_bytes as u128 {
1082 return Err(MemoryPressure {
1083 layer: "manager_check_budget",
1084 current_bytes: current as u128,
1085 requested_bytes: bytes as u128,
1086 budget_bytes: self.budget.device_bytes,
1087 prior_peak_bytes: self.accounting.peak.load(Ordering::SeqCst),
1088 }
1089 .into_error());
1090 }
1091
1092 Ok(())
1093 }
1094
1095 pub fn allocated_bytes(&self) -> u64 {
1097 self.accounting.allocated.load(Ordering::SeqCst)
1098 }
1099
1100 pub fn deallocation_failure_count(&self) -> u64 {
1103 self.accounting
1104 .deallocation_failure_count
1105 .load(Ordering::SeqCst)
1106 }
1107
1108 pub fn deallocation_failure_bytes(&self) -> u64 {
1111 self.accounting
1112 .deallocation_failure_bytes
1113 .load(Ordering::SeqCst)
1114 }
1115
1116 pub fn peak_bytes(&self) -> u64 {
1120 self.accounting.peak.load(Ordering::SeqCst)
1121 }
1122
1123 pub fn reset_peak(&self) {
1127 self.accounting.peak.store(
1128 self.accounting.allocated.load(Ordering::SeqCst),
1129 Ordering::SeqCst,
1130 );
1131 }
1132
1133 pub fn alloc_count(&self) -> u64 {
1137 self.accounting.alloc_count.load(Ordering::Relaxed)
1138 }
1139
1140 pub fn reset_alloc_count(&self) {
1142 self.accounting.alloc_count.store(0, Ordering::Relaxed);
1143 }
1144
1145 pub fn budget(&self) -> &MemoryBudget {
1147 &self.budget
1148 }
1149
1150 pub fn budget_limit_bytes(&self) -> u64 {
1152 self.budget.device_bytes
1153 }
1154
1155 pub fn device(&self) -> &Arc<CudaDevice> {
1157 &self.device
1158 }
1159
1160 pub fn record_free(&self, bytes: u64) -> Result<()> {
1167 let _mutation = self
1168 .accounting
1169 .mutation_lock
1170 .lock()
1171 .expect("GPU memory accounting poisoned");
1172 let admitted = self.accounting.allocated.load(Ordering::SeqCst);
1173 let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
1174 if admitted != 0 || reserved != 0 {
1175 return Err(XlogError::Kernel(format!(
1176 "manual GPU accounting release refused with live tracked bytes: admitted_bytes={} reserved_bytes={}",
1177 admitted, reserved
1178 )));
1179 }
1180 if bytes != 0 {
1181 return Err(XlogError::Kernel(format!(
1182 "manual GPU accounting release underflow: current_bytes=0 requested_bytes={}",
1183 bytes
1184 )));
1185 }
1186 Ok(())
1187 }
1188
1189 pub fn alloc_raw(self: &Arc<Self>, bytes: usize, tag: AllocTag) -> Result<RuntimeAllocBlock> {
1211 if self.runtime.is_none() {
1212 return Err(XlogError::Kernel(
1213 "GpuMemoryManager::alloc_raw called without an attached XlogDeviceRuntime; \
1214 construct via with_runtime to enable runtime routing"
1215 .to_string(),
1216 ));
1217 }
1218 let bytes_u64 = u64::try_from(bytes)
1219 .map_err(|_| XlogError::Kernel("Allocation size overflow".to_string()))?;
1220 self.reserve_local_bytes(bytes_u64, "manager_alloc_raw")?;
1221 match self.alloc_raw_after_local_reservation(bytes, bytes_u64, tag, None) {
1222 Ok(allocation) => Ok(allocation),
1223 Err(error) => {
1224 self.rollback_local_reservation(bytes_u64)?;
1225 Err(error)
1226 }
1227 }
1228 }
1229
1230 fn alloc_raw_after_local_reservation(
1231 self: &Arc<Self>,
1232 bytes: usize,
1233 bytes_u64: u64,
1234 tag: AllocTag,
1235 runtime_reservation: Option<&mut RuntimeMemoryReservation>,
1236 ) -> Result<RuntimeAllocBlock> {
1237 let runtime = self.runtime.as_ref().ok_or_else(|| {
1238 XlogError::Kernel(
1239 "GpuMemoryManager::alloc_raw called without an attached XlogDeviceRuntime; \
1240 construct via with_runtime to enable runtime routing"
1241 .to_string(),
1242 )
1243 })?;
1244
1245 #[cfg(test)]
1246 self.run_after_local_reservation_hook(bytes_u64);
1247
1248 let allocation = match runtime_reservation {
1253 Some(reservation) => reservation.allocate(bytes, StreamId::DEFAULT, tag),
1254 None => runtime.allocate(bytes, StreamId::DEFAULT, tag),
1255 };
1256 match allocation {
1257 Ok(block) => {
1258 self.publish_admission(bytes_u64);
1259 Ok(RuntimeAllocBlock {
1260 bytes: bytes_u64,
1261 manager: Arc::clone(self),
1262 runtime: Arc::clone(runtime),
1263 block: Some(block),
1264 })
1265 }
1266 Err(e) => {
1267 let prior_peak = self.accounting.peak.load(Ordering::SeqCst);
1268 Err(map_resource_error(e, prior_peak))
1269 }
1270 }
1271 }
1272
1273 pub fn remaining_bytes(&self) -> u64 {
1275 let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
1276 self.budget.device_bytes.saturating_sub(reserved)
1277 }
1278
1279 pub fn reset_tracking(&self) -> Result<()> {
1281 let _mutation = self
1282 .accounting
1283 .mutation_lock
1284 .lock()
1285 .expect("GPU memory accounting poisoned");
1286 let admitted = self.accounting.allocated.load(Ordering::SeqCst);
1287 let reserved = self.accounting.budget_reserved.load(Ordering::SeqCst);
1288 if admitted != 0 || reserved != 0 {
1289 return Err(XlogError::Kernel(format!(
1290 "GPU accounting reset refused with live tracked bytes: admitted_bytes={} reserved_bytes={}",
1291 admitted, reserved
1292 )));
1293 }
1294 self.accounting.peak.store(0, Ordering::SeqCst);
1295 Ok(())
1296 }
1297
1298 #[cfg(test)]
1299 fn run_after_local_reservation_hook(&self, bytes: u64) {
1300 let hook = self
1301 .after_local_reservation_hook
1302 .lock()
1303 .expect("after-local-reservation test hook poisoned")
1304 .clone();
1305 if let Some(hook) = hook {
1306 hook(bytes);
1307 }
1308 }
1309}
1310
1311fn map_resource_error(e: ResourceError, prior_peak_bytes: u64) -> XlogError {
1312 match e {
1313 ResourceError::OutOfBudget {
1314 requested,
1315 current,
1316 limit,
1317 ..
1318 } => MemoryPressure {
1319 layer: "device_runtime",
1320 current_bytes: current as u128,
1321 requested_bytes: requested as u128,
1322 budget_bytes: u64::try_from(limit).unwrap_or(u64::MAX),
1323 prior_peak_bytes,
1324 }
1325 .into_error(),
1326 ResourceError::Driver(msg) => XlogError::Kernel(format!("device-runtime driver: {}", msg)),
1327 ResourceError::StreamMisuse(msg) => {
1328 XlogError::Kernel(format!("device-runtime stream misuse: {}", msg))
1329 }
1330 ResourceError::UseAfterFree { generation } => XlogError::Kernel(format!(
1331 "device-runtime use-after-free on generation {:?}",
1332 generation
1333 )),
1334 ResourceError::OutOfBounds { generation } => XlogError::Kernel(format!(
1335 "device-runtime out-of-bounds on generation {:?}",
1336 generation
1337 )),
1338 }
1339}
1340
1341pub struct RuntimeAllocBlock {
1356 bytes: u64,
1357 manager: Arc<GpuMemoryManager>,
1358 runtime: Arc<XlogDeviceRuntime>,
1359 block: Option<DeviceBlock>,
1363}
1364
1365impl RuntimeAllocBlock {
1366 pub fn ptr(&self) -> u64 {
1369 self.block
1370 .as_ref()
1371 .expect("RuntimeAllocBlock used after drop")
1372 .ptr
1373 }
1374
1375 pub fn bytes(&self) -> usize {
1377 self.bytes as usize
1378 }
1379
1380 pub fn device_block(&self) -> &DeviceBlock {
1383 self.block
1384 .as_ref()
1385 .expect("RuntimeAllocBlock used after drop")
1386 }
1387}
1388
1389impl std::fmt::Debug for RuntimeAllocBlock {
1390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1391 let mut dbg = f.debug_struct("RuntimeAllocBlock");
1392 dbg.field("bytes", &self.bytes);
1393 match &self.block {
1394 Some(b) => {
1395 dbg.field("ptr", &format_args!("{:#x}", b.ptr));
1396 dbg.field("device_ordinal", &b.device_ordinal);
1397 dbg.field("alloc_stream", &b.alloc_stream);
1398 dbg.field("tag", &b.tag);
1399 dbg.field("generation", &b.generation);
1400 dbg.field("state", &b.state);
1401 }
1402 None => {
1403 dbg.field("block", &"<dropped>");
1404 }
1405 }
1406 dbg.finish()
1407 }
1408}
1409
1410impl Drop for RuntimeAllocBlock {
1411 fn drop(&mut self) {
1412 if let Some(block) = self.block.take() {
1413 match self.runtime.deallocate(block) {
1414 Ok(()) => {
1415 let release = self.manager.release_owned_allocation(self.bytes);
1416 debug_assert!(release.is_ok(), "allocation release must be balanced");
1417 }
1418 Err(_) => self.manager.record_deallocation_failure(self.bytes),
1419 }
1420 }
1421 }
1422}
1423
1424pub enum CudaColumn {
1430 Owned(TrackedCudaSlice<u8>),
1431 Dlpack(DlpackColumn),
1432 ArrowDevice(ArrowDeviceColumn),
1433}
1434
1435pub struct DlpackColumn {
1436 ptr: cudarc::driver::sys::CUdeviceptr,
1437 len_bytes: usize,
1438 stream: Arc<CudaStream>,
1439 _tensor: DlpackManagedTensor,
1440 source_slice: Option<Arc<TrackedCudaSlice<u8>>>,
1454}
1455
1456pub struct ArrowDeviceColumn {
1457 ptr: cudarc::driver::sys::CUdeviceptr,
1458 len_bytes: usize,
1459 stream: Arc<CudaStream>,
1460 _import: Arc<ArrowDeviceImport>,
1461 source_slice: Option<Arc<TrackedCudaSlice<u8>>>,
1465}
1466
1467impl CudaColumn {
1468 pub fn owned(slice: TrackedCudaSlice<u8>) -> Self {
1469 Self::Owned(slice)
1470 }
1471
1472 pub fn dlpack(
1473 ptr: cudarc::driver::sys::CUdeviceptr,
1474 len_bytes: usize,
1475 stream: Arc<CudaStream>,
1476 tensor: DlpackManagedTensor,
1477 ) -> Self {
1478 Self::Dlpack(DlpackColumn {
1479 ptr,
1480 len_bytes,
1481 stream,
1482 _tensor: tensor,
1483 source_slice: None,
1484 })
1485 }
1486
1487 pub fn dlpack_xlog_owned(
1504 source_slice: Arc<TrackedCudaSlice<u8>>,
1505 stream: Arc<CudaStream>,
1506 tensor: DlpackManagedTensor,
1507 ) -> Self {
1508 let ptr = *source_slice.device_ptr();
1509 let len_bytes = source_slice.len();
1510 Self::Dlpack(DlpackColumn {
1511 ptr,
1512 len_bytes,
1513 stream,
1514 _tensor: tensor,
1515 source_slice: Some(source_slice),
1516 })
1517 }
1518
1519 pub fn arrow_device(
1520 ptr: cudarc::driver::sys::CUdeviceptr,
1521 len_bytes: usize,
1522 stream: Arc<CudaStream>,
1523 import: Arc<ArrowDeviceImport>,
1524 ) -> Self {
1525 Self::ArrowDevice(ArrowDeviceColumn {
1526 ptr,
1527 len_bytes,
1528 stream,
1529 _import: import,
1530 source_slice: None,
1531 })
1532 }
1533
1534 pub fn arrow_device_xlog_owned(
1544 source_slice: Arc<TrackedCudaSlice<u8>>,
1545 stream: Arc<CudaStream>,
1546 import: Arc<ArrowDeviceImport>,
1547 ) -> Self {
1548 let ptr = *source_slice.device_ptr();
1549 let len_bytes = source_slice.len();
1550 Self::ArrowDevice(ArrowDeviceColumn {
1551 ptr,
1552 len_bytes,
1553 stream,
1554 _import: import,
1555 source_slice: Some(source_slice),
1556 })
1557 }
1558
1559 pub fn stream(&self) -> &Arc<CudaStream> {
1560 match self {
1561 CudaColumn::Owned(slice) => slice.stream(),
1562 CudaColumn::Dlpack(col) => &col.stream,
1563 CudaColumn::ArrowDevice(col) => &col.stream,
1564 }
1565 }
1566
1567 pub fn device_ptr(&self) -> &cudarc::driver::sys::CUdeviceptr {
1568 match self {
1569 CudaColumn::Owned(slice) => slice.device_ptr(),
1570 CudaColumn::Dlpack(col) => &col.ptr,
1571 CudaColumn::ArrowDevice(col) => &col.ptr,
1572 }
1573 }
1574
1575 pub fn memory_manager_ptr_value(&self) -> Option<usize> {
1580 match self {
1581 CudaColumn::Owned(slice) => Some(slice.memory_manager_ptr_value()),
1582 CudaColumn::Dlpack(col) => col
1583 .source_slice
1584 .as_ref()
1585 .map(|slice| slice.memory_manager_ptr_value()),
1586 CudaColumn::ArrowDevice(col) => col
1587 .source_slice
1588 .as_ref()
1589 .map(|slice| slice.memory_manager_ptr_value()),
1590 }
1591 }
1592
1593 pub(crate) fn runtime_allocation_identity(&self) -> Result<Option<RuntimeAllocationIdentity>> {
1594 match self {
1595 CudaColumn::Owned(slice) => slice.runtime_allocation_identity(),
1596 CudaColumn::Dlpack(col) => {
1597 let Some(source) = &col.source_slice else {
1598 return Ok(None);
1599 };
1600 let mut identity = source.runtime_allocation_identity()?;
1601 if let Some(identity) = &mut identity {
1602 identity.allocation_ptr = col.ptr;
1603 identity.allocation_bytes = col.len_bytes;
1604 }
1605 Ok(identity)
1606 }
1607 CudaColumn::ArrowDevice(col) => {
1608 let Some(source) = &col.source_slice else {
1609 return Ok(None);
1610 };
1611 let mut identity = source.runtime_allocation_identity()?;
1612 if let Some(identity) = &mut identity {
1613 identity.allocation_ptr = col.ptr;
1614 identity.allocation_bytes = col.len_bytes;
1615 }
1616 Ok(identity)
1617 }
1618 }
1619 }
1620
1621 pub fn runtime_block(&self) -> Option<&crate::device_runtime::DeviceBlock> {
1636 match self {
1637 CudaColumn::Owned(slice) => slice.runtime_block(),
1638 CudaColumn::Dlpack(col) => col.source_slice.as_ref().and_then(|s| s.runtime_block()),
1639 CudaColumn::ArrowDevice(col) => {
1640 col.source_slice.as_ref().and_then(|s| s.runtime_block())
1641 }
1642 }
1643 }
1644
1645 pub fn is_external(&self) -> bool {
1660 match self {
1661 CudaColumn::Owned(_) => false,
1662 CudaColumn::Dlpack(col) => col.source_slice.is_none(),
1663 CudaColumn::ArrowDevice(col) => col.source_slice.is_none(),
1664 }
1665 }
1666}
1667
1668impl From<TrackedCudaSlice<u8>> for CudaColumn {
1669 fn from(value: TrackedCudaSlice<u8>) -> Self {
1670 CudaColumn::Owned(value)
1671 }
1672}
1673
1674impl DeviceSlice<u8> for CudaColumn {
1675 fn len(&self) -> usize {
1676 match self {
1677 CudaColumn::Owned(slice) => slice.len(),
1678 CudaColumn::Dlpack(col) => col.len_bytes,
1679 CudaColumn::ArrowDevice(col) => col.len_bytes,
1680 }
1681 }
1682
1683 fn stream(&self) -> &Arc<CudaStream> {
1684 self.stream()
1685 }
1686}
1687
1688impl DevicePtr<u8> for CudaColumn {
1689 fn device_ptr<'a>(
1690 &'a self,
1691 stream: &'a CudaStream,
1692 ) -> (cudarc::driver::sys::CUdeviceptr, SyncOnDrop<'a>) {
1693 match self {
1694 CudaColumn::Owned(slice) => DevicePtr::device_ptr(slice, stream),
1695 CudaColumn::Dlpack(col) => (col.ptr, SyncOnDrop::Sync(None)),
1696 CudaColumn::ArrowDevice(col) => (col.ptr, SyncOnDrop::Sync(None)),
1697 }
1698 }
1699}
1700
1701impl DevicePtrMut<u8> for CudaColumn {
1702 fn device_ptr_mut<'a>(
1703 &'a mut self,
1704 stream: &'a CudaStream,
1705 ) -> (cudarc::driver::sys::CUdeviceptr, SyncOnDrop<'a>) {
1706 match self {
1707 CudaColumn::Owned(slice) => DevicePtrMut::device_ptr_mut(slice, stream),
1708 CudaColumn::Dlpack(col) => (col.ptr, SyncOnDrop::Sync(None)),
1709 CudaColumn::ArrowDevice(col) => (col.ptr, SyncOnDrop::Sync(None)),
1710 }
1711 }
1712}
1713
1714impl AsKernelParam for &CudaColumn {
1715 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
1716 ((self.device_ptr()) as *const cudarc::driver::sys::CUdeviceptr)
1717 .cast_mut()
1718 .cast()
1719 }
1720}
1721
1722impl AsKernelParam for &mut CudaColumn {
1723 fn as_kernel_param(&self) -> *mut std::ffi::c_void {
1724 ((self.device_ptr()) as *const cudarc::driver::sys::CUdeviceptr)
1725 .cast_mut()
1726 .cast()
1727 }
1728}
1729
1730impl<'a> IntoKernelParamStorage for &'a CudaColumn {
1731 type Storage = DeviceParamStorage<'a>;
1732
1733 fn into_kernel_param_storage(self) -> Self::Storage {
1734 match self {
1735 CudaColumn::Owned(slice) => slice.into_kernel_param_storage(),
1736 CudaColumn::Dlpack(col) => DeviceParamStorage::unsynced(col.ptr),
1737 CudaColumn::ArrowDevice(col) => DeviceParamStorage::unsynced(col.ptr),
1738 }
1739 }
1740}
1741
1742impl<'a> IntoKernelParamStorage for &'a mut CudaColumn {
1743 type Storage = DeviceParamStorage<'a>;
1744
1745 fn into_kernel_param_storage(self) -> Self::Storage {
1746 match self {
1747 CudaColumn::Owned(slice) => slice.into_kernel_param_storage(),
1748 CudaColumn::Dlpack(col) => DeviceParamStorage::unsynced(col.ptr),
1749 CudaColumn::ArrowDevice(col) => DeviceParamStorage::unsynced(col.ptr),
1750 }
1751 }
1752}
1753
1754pub struct CudaBuffer {
1759 pub(crate) columns: Vec<CudaColumn>,
1761 pub(crate) row_cap: u64,
1763 pub(crate) d_num_rows: TrackedCudaSlice<u32>,
1765 pub(crate) schema: Schema,
1767 cached_row_count: AtomicU32,
1771 canonical_full_row_set_certified: bool,
1774}
1775
1776impl CudaBuffer {
1777 pub fn from_columns(
1788 columns: Vec<CudaColumn>,
1789 row_cap: u64,
1790 d_num_rows: TrackedCudaSlice<u32>,
1791 schema: Schema,
1792 ) -> Self {
1793 assert_eq!(
1794 columns.len(),
1795 schema.arity(),
1796 "Number of columns ({}) must match schema arity ({})",
1797 columns.len(),
1798 schema.arity()
1799 );
1800 Self {
1801 columns,
1802 row_cap,
1803 d_num_rows,
1804 schema,
1805 cached_row_count: AtomicU32::new(u32::MAX),
1806 canonical_full_row_set_certified: false,
1807 }
1808 }
1809
1810 pub fn from_columns_with_host_count(
1813 columns: Vec<CudaColumn>,
1814 row_cap: u64,
1815 d_num_rows: TrackedCudaSlice<u32>,
1816 schema: Schema,
1817 host_row_count: u32,
1818 ) -> Self {
1819 assert_eq!(
1820 columns.len(),
1821 schema.arity(),
1822 "Number of columns ({}) must match schema arity ({})",
1823 columns.len(),
1824 schema.arity()
1825 );
1826 Self {
1827 columns,
1828 row_cap,
1829 d_num_rows,
1830 schema,
1831 cached_row_count: AtomicU32::new(host_row_count),
1832 canonical_full_row_set_certified: false,
1833 }
1834 }
1835
1836 pub fn cached_row_count(&self) -> Option<u32> {
1838 let v = self.cached_row_count.load(Ordering::Relaxed);
1839 if v == u32::MAX {
1840 None
1841 } else {
1842 Some(v)
1843 }
1844 }
1845
1846 pub(crate) fn set_cached_row_count_if_unset(&self, count: u32) {
1849 let _ = self.cached_row_count.compare_exchange(
1850 u32::MAX,
1851 count,
1852 Ordering::Relaxed,
1853 Ordering::Relaxed,
1854 );
1855 }
1856
1857 pub fn canonical_full_row_set_certified(&self) -> bool {
1859 self.canonical_full_row_set_certified
1860 }
1861
1862 pub(crate) fn certify_canonical_full_row_set(&mut self) {
1864 self.canonical_full_row_set_certified = true;
1865 }
1866
1867 pub fn columns(&self) -> &[CudaColumn] {
1869 &self.columns
1870 }
1871
1872 pub fn columns_mut(&mut self) -> &mut [CudaColumn] {
1874 self.canonical_full_row_set_certified = false;
1875 &mut self.columns
1876 }
1877
1878 pub fn set_schema(&mut self, schema: Schema) {
1880 assert_eq!(self.columns.len(), schema.arity());
1881 self.schema = schema;
1882 self.canonical_full_row_set_certified = false;
1883 }
1884
1885 pub fn set_row_capacity(&mut self, row_cap: u64) {
1887 self.row_cap = row_cap;
1888 self.cached_row_count.store(u32::MAX, Ordering::Relaxed);
1889 self.canonical_full_row_set_certified = false;
1890 }
1891
1892 pub fn num_rows_device_mut(&mut self) -> &mut TrackedCudaSlice<u32> {
1894 self.cached_row_count.store(u32::MAX, Ordering::Relaxed);
1895 self.canonical_full_row_set_certified = false;
1896 &mut self.d_num_rows
1897 }
1898
1899 pub fn num_rows(&self) -> u64 {
1901 self.row_cap
1902 }
1903
1904 pub fn num_rows_device(&self) -> &TrackedCudaSlice<u32> {
1906 &self.d_num_rows
1907 }
1908
1909 pub fn is_empty(&self) -> bool {
1911 self.row_cap == 0
1912 }
1913
1914 pub fn schema(&self) -> &Schema {
1916 &self.schema
1917 }
1918
1919 pub fn arity(&self) -> usize {
1921 self.schema.arity()
1922 }
1923
1924 pub fn estimated_bytes(&self) -> u64 {
1926 self.row_cap * self.schema.row_size_bytes() as u64
1927 }
1928
1929 pub fn column(&self, index: usize) -> Option<&CudaColumn> {
1931 self.columns.get(index)
1932 }
1933}
1934
1935pub fn validate_logical_row_count(row_cap: u64, logical_rows: usize) -> Result<usize> {
1936 let row_cap_usize = usize::try_from(row_cap)
1937 .map_err(|_| XlogError::Kernel(format!("Row capacity {} exceeds usize::MAX", row_cap)))?;
1938 if logical_rows > row_cap_usize {
1939 return Err(XlogError::Kernel(format!(
1940 "Logical row count {} exceeds row capacity {}",
1941 logical_rows, row_cap
1942 )));
1943 }
1944 debug_assert!(logical_rows <= row_cap_usize);
1945 Ok(logical_rows)
1946}
1947
1948#[cfg(test)]
1949mod tests {
1950 use super::*;
1951 use crate::device_runtime::{DeviceMemoryResource, DirectCudaResource, ResourceResult};
1952 use xlog_core::ScalarType;
1953
1954 #[test]
1955 fn reservation_exposes_exact_memory_manager_identity() {
1956 let _: fn(&GpuMemoryReservation) -> usize = GpuMemoryReservation::memory_manager_ptr_value;
1957 }
1958
1959 #[test]
1960 fn cuda_column_exposes_optional_memory_manager_identity() {
1961 let _: fn(&CudaColumn) -> Option<usize> = CudaColumn::memory_manager_ptr_value;
1962 }
1963
1964 #[test]
1965 fn tracked_slice_exposes_runtime_allocation_identity_snapshot() {
1966 let _: fn(&TrackedCudaSlice<u32>) -> Result<Option<RuntimeAllocationIdentity>> =
1967 TrackedCudaSlice::<u32>::runtime_allocation_identity;
1968 }
1969
1970 #[test]
1971 fn cuda_column_exposes_runtime_allocation_identity_snapshot() {
1972 let _: fn(&CudaColumn) -> Result<Option<RuntimeAllocationIdentity>> =
1973 CudaColumn::runtime_allocation_identity;
1974 }
1975
1976 struct FailAfterDeallocateResource {
1977 inner: DirectCudaResource,
1978 deallocate_calls: Arc<AtomicU64>,
1979 }
1980
1981 struct FailFirstAllocationResource {
1982 inner: DirectCudaResource,
1983 fail_next: std::sync::atomic::AtomicBool,
1984 }
1985
1986 impl DeviceMemoryResource for FailFirstAllocationResource {
1987 fn allocate(
1988 &self,
1989 bytes: usize,
1990 stream: StreamId,
1991 tag: AllocTag,
1992 ) -> ResourceResult<DeviceBlock> {
1993 if self.fail_next.swap(false, Ordering::SeqCst) {
1994 return Err(ResourceError::Driver(
1995 "injected allocation failure".to_string(),
1996 ));
1997 }
1998 self.inner.allocate(bytes, stream, tag)
1999 }
2000
2001 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
2002 self.inner.deallocate(block)
2003 }
2004
2005 fn device_ordinal(&self) -> u32 {
2006 self.inner.device_ordinal()
2007 }
2008
2009 fn bytes_outstanding(&self) -> usize {
2010 self.inner.bytes_outstanding()
2011 }
2012 }
2013
2014 impl DeviceMemoryResource for FailAfterDeallocateResource {
2015 fn allocate(
2016 &self,
2017 bytes: usize,
2018 stream: StreamId,
2019 tag: AllocTag,
2020 ) -> ResourceResult<DeviceBlock> {
2021 self.inner.allocate(bytes, stream, tag)
2022 }
2023
2024 fn deallocate(&self, block: DeviceBlock) -> ResourceResult<()> {
2025 self.inner.deallocate(block)?;
2026 self.deallocate_calls.fetch_add(1, Ordering::SeqCst);
2027 Err(ResourceError::Driver(
2028 "injected deallocation completion failure".to_string(),
2029 ))
2030 }
2031
2032 fn device_ordinal(&self) -> u32 {
2033 self.inner.device_ordinal()
2034 }
2035
2036 fn bytes_outstanding(&self) -> usize {
2037 self.inner.bytes_outstanding()
2038 }
2039 }
2040
2041 fn try_device() -> Option<Arc<CudaDevice>> {
2042 match CudaDevice::new(0) {
2043 Ok(d) => Some(Arc::new(d)),
2044 Err(e) if std::env::var("XLOG_REQUIRE_CUDA").as_deref() == Ok("1") => {
2045 panic!("XLOG_REQUIRE_CUDA=1 but CUDA initialization failed: {e}")
2046 }
2047 Err(e) => {
2048 eprintln!("Skipping test: CUDA runtime unavailable: {}", e);
2049 None
2050 }
2051 }
2052 }
2053
2054 fn assert_memory_pressure(
2055 error: XlogError,
2056 expected_context: &str,
2057 expected_required: u64,
2058 expected_budget: u64,
2059 ) {
2060 match error {
2061 XlogError::ResourceExhausted {
2062 context,
2063 estimated_bytes,
2064 budget_bytes,
2065 } => {
2066 assert_eq!(context, expected_context);
2067 assert_eq!(estimated_bytes, expected_required);
2068 assert_eq!(budget_bytes, expected_budget);
2069 }
2070 other => panic!("expected ResourceExhausted, got {other:?}"),
2071 }
2072 }
2073
2074 #[test]
2076 fn test_cuda_buffer_empty() {
2077 let Some(device) = try_device() else {
2078 return;
2079 };
2080 let budget = MemoryBudget::with_limit(1024 * 1024);
2081 let manager = Arc::new(GpuMemoryManager::new(device, budget));
2082 let mut d_num_rows = manager.alloc::<u32>(1).unwrap();
2083 manager
2084 .device()
2085 .inner()
2086 .htod_sync_copy_into(&[0u32], &mut d_num_rows)
2087 .unwrap();
2088 let buffer = CudaBuffer::from_columns(Vec::new(), 0, d_num_rows, Schema::new(vec![]));
2089 assert!(buffer.is_empty());
2090 assert_eq!(buffer.num_rows(), 0);
2091 assert_eq!(buffer.arity(), 0);
2092 assert_eq!(buffer.estimated_bytes(), 0);
2093 }
2094
2095 #[test]
2096 fn test_cuda_buffer_schema() {
2097 let schema = Schema::new(vec![
2098 ("a".to_string(), ScalarType::U32),
2099 ("b".to_string(), ScalarType::U64),
2100 ]);
2101
2102 let Some(device) = try_device() else {
2103 return;
2104 };
2105 let budget = MemoryBudget::with_limit(1024 * 1024);
2106 let manager = Arc::new(GpuMemoryManager::new(device, budget));
2107 let mut d_num_rows = manager.alloc::<u32>(1).unwrap();
2108 manager
2109 .device()
2110 .inner()
2111 .htod_sync_copy_into(&[100u32], &mut d_num_rows)
2112 .unwrap();
2113
2114 let col_a = CudaColumn::owned(manager.alloc::<u8>(100 * 4).unwrap()); let col_b = CudaColumn::owned(manager.alloc::<u8>(100 * 8).unwrap()); let buffer = CudaBuffer::from_columns(vec![col_a, col_b], 100, d_num_rows, schema.clone());
2118
2119 assert_eq!(buffer.num_rows(), 100);
2120 assert_eq!(buffer.arity(), 2);
2121 assert_eq!(buffer.estimated_bytes(), 1200);
2123 assert_eq!(buffer.schema(), &schema);
2124 }
2125
2126 #[test]
2128 fn test_memory_manager_creation() {
2129 let Some(device) = try_device() else {
2130 return;
2131 };
2132 let budget = MemoryBudget::with_limit(1024 * 1024); let manager = Arc::new(GpuMemoryManager::new(device, budget));
2134
2135 assert_eq!(manager.allocated_bytes(), 0);
2136 assert_eq!(manager.budget().device_bytes, 1024 * 1024);
2137 assert_eq!(manager.remaining_bytes(), 1024 * 1024);
2138 }
2139
2140 #[test]
2141 fn test_memory_manager_alloc() {
2142 let Some(device) = try_device() else {
2143 return;
2144 };
2145 let budget = MemoryBudget::with_limit(1024 * 1024); let manager = Arc::new(GpuMemoryManager::new(device, budget));
2147
2148 let _slice = manager
2150 .alloc::<u32>(256)
2151 .expect("Allocation should succeed");
2152
2153 assert_eq!(manager.allocated_bytes(), 1024);
2154 assert_eq!(manager.remaining_bytes(), 1024 * 1024 - 1024);
2155 }
2156
2157 #[test]
2158 fn test_memory_manager_budget_exceeded() {
2159 let Some(device) = try_device() else {
2160 return;
2161 };
2162 let budget = MemoryBudget::with_limit(1024); let manager = Arc::new(GpuMemoryManager::new(device, budget));
2164
2165 let result = manager.alloc::<u32>(512);
2167
2168 assert!(result.is_err());
2169 if let Err(XlogError::ResourceExhausted {
2170 estimated_bytes,
2171 budget_bytes,
2172 ..
2173 }) = result
2174 {
2175 assert_eq!(estimated_bytes, 2048);
2176 assert_eq!(budget_bytes, 1024);
2177 } else {
2178 panic!("Expected ResourceExhausted error");
2179 }
2180 }
2181
2182 #[test]
2183 fn test_memory_manager_check_budget() {
2184 let Some(device) = try_device() else {
2185 return;
2186 };
2187 let budget = MemoryBudget::with_limit(1000);
2188 let manager = Arc::new(GpuMemoryManager::new(device, budget));
2189
2190 assert!(manager.check_budget(500).is_ok());
2192
2193 assert!(manager.check_budget(1001).is_err());
2195 }
2196
2197 #[test]
2198 fn check_budget_does_not_reserve_a_multi_allocation_request() {
2199 let Some(device) = try_device() else {
2200 return;
2201 };
2202 let manager = Arc::new(GpuMemoryManager::new(
2203 device,
2204 MemoryBudget::with_limit(8192),
2205 ));
2206 let checked = Arc::new(std::sync::Barrier::new(2));
2207 let competitor_allocated = Arc::new(std::sync::Barrier::new(2));
2208 let release_competitor = Arc::new(std::sync::Barrier::new(2));
2209
2210 let checked_in_thread = Arc::clone(&checked);
2211 let allocated_in_thread = Arc::clone(&competitor_allocated);
2212 let release_in_thread = Arc::clone(&release_competitor);
2213 let competitor_manager = Arc::clone(&manager);
2214 let competitor = std::thread::spawn(move || {
2215 checked_in_thread.wait();
2216 let allocation = competitor_manager
2217 .alloc::<u8>(4096)
2218 .expect("competing allocation must fit after the check-only query");
2219 allocated_in_thread.wait();
2220 release_in_thread.wait();
2221 drop(allocation);
2222 });
2223
2224 manager
2225 .check_budget(8192)
2226 .expect("the complete request fits before the competing allocation");
2227 checked.wait();
2228 competitor_allocated.wait();
2229 let first = manager
2230 .alloc::<u8>(4096)
2231 .expect("the first materialized allocation must fit");
2232 let second = manager.alloc::<u8>(4096);
2233 assert!(
2234 matches!(second, Err(XlogError::ResourceExhausted { .. })),
2235 "a check-only query cannot protect later allocations from a competitor"
2236 );
2237
2238 release_competitor.wait();
2239 competitor.join().expect("competitor thread panicked");
2240 drop(first);
2241 assert_eq!(manager.allocated_bytes(), 0);
2242 assert_eq!(manager.remaining_bytes(), 8192);
2243 }
2244
2245 #[test]
2246 fn reservation_rejects_the_complete_request_before_any_allocation() {
2247 let Some(device) = try_device() else {
2248 return;
2249 };
2250 let manager = Arc::new(GpuMemoryManager::new(
2251 device,
2252 MemoryBudget::with_limit(4095),
2253 ));
2254 manager.reset_alloc_count();
2255
2256 let error = manager
2257 .reserve_bytes(4096)
2258 .expect_err("a request one byte above the budget must be rejected atomically");
2259
2260 assert_memory_pressure(
2261 error,
2262 "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",
2263 4096,
2264 4095,
2265 );
2266 assert_eq!(manager.alloc_count(), 0);
2267 assert_eq!(manager.allocated_bytes(), 0);
2268 assert_eq!(manager.peak_bytes(), 0);
2269 assert_eq!(manager.remaining_bytes(), 4095);
2270 }
2271
2272 #[test]
2273 fn competing_complete_reservations_cannot_both_claim_the_budget() {
2274 let Some(device) = try_device() else {
2275 return;
2276 };
2277 let manager = Arc::new(GpuMemoryManager::new(
2278 device,
2279 MemoryBudget::with_limit(4096),
2280 ));
2281 let attempted = Arc::new(std::sync::Barrier::new(2));
2282
2283 let reserve = |manager: Arc<GpuMemoryManager>, attempted: Arc<std::sync::Barrier>| {
2284 std::thread::spawn(move || {
2285 let reservation = manager.reserve_bytes(4096);
2286 attempted.wait();
2287 let admitted = reservation.is_ok();
2288 drop(reservation);
2289 admitted
2290 })
2291 };
2292 let left = reserve(Arc::clone(&manager), Arc::clone(&attempted));
2293 let right = reserve(Arc::clone(&manager), Arc::clone(&attempted));
2294
2295 let admitted = [
2296 left.join().expect("left reservation thread panicked"),
2297 right.join().expect("right reservation thread panicked"),
2298 ];
2299 assert_eq!(admitted.into_iter().filter(|value| *value).count(), 1);
2300 assert_eq!(manager.allocated_bytes(), 0);
2301 assert_eq!(manager.remaining_bytes(), 4096);
2302 }
2303
2304 #[test]
2305 fn competing_managers_share_one_runtime_reservation_budget() {
2306 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
2307 return;
2308 };
2309 let left_manager = Arc::new(GpuMemoryManager::with_runtime(
2310 Arc::clone(&device),
2311 MemoryBudget::with_limit(4096),
2312 Arc::clone(&runtime),
2313 ));
2314 let right_manager = Arc::new(GpuMemoryManager::with_runtime(
2315 device,
2316 MemoryBudget::with_limit(4096),
2317 runtime,
2318 ));
2319 let attempted = Arc::new(std::sync::Barrier::new(2));
2320
2321 let reserve = |manager: Arc<GpuMemoryManager>, attempted: Arc<std::sync::Barrier>| {
2322 std::thread::spawn(move || {
2323 let reservation = manager.reserve_bytes(4096);
2324 attempted.wait();
2325 let admitted = reservation.is_ok();
2326 drop(reservation);
2327 admitted
2328 })
2329 };
2330 let left = reserve(left_manager, Arc::clone(&attempted));
2331 let right = reserve(right_manager, attempted);
2332
2333 let admitted = [
2334 left.join().expect("left reservation thread panicked"),
2335 right.join().expect("right reservation thread panicked"),
2336 ];
2337 assert_eq!(admitted.into_iter().filter(|value| *value).count(), 1);
2338 }
2339
2340 #[test]
2341 fn runtime_reservation_blocks_competing_ordinary_allocation_until_materialized() {
2342 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
2343 return;
2344 };
2345 let reserving_manager = Arc::new(GpuMemoryManager::with_runtime(
2346 Arc::clone(&device),
2347 MemoryBudget::with_limit(4096),
2348 Arc::clone(&runtime),
2349 ));
2350 let competing_manager = Arc::new(GpuMemoryManager::with_runtime(
2351 device,
2352 MemoryBudget::with_limit(4096),
2353 runtime,
2354 ));
2355 let mut reservation = reserving_manager
2356 .reserve_bytes(4096)
2357 .expect("complete runtime reservation");
2358
2359 assert!(matches!(
2360 competing_manager.alloc::<u8>(1),
2361 Err(XlogError::ResourceExhausted { .. })
2362 ));
2363 assert_eq!(competing_manager.allocated_bytes(), 0);
2364 assert_eq!(competing_manager.remaining_bytes(), 4096);
2365
2366 let allocation = reservation
2367 .alloc::<u8>(4096)
2368 .expect("reserved bytes cannot be stolen by an ordinary allocator");
2369 assert_eq!(reserving_manager.allocated_bytes(), 4096);
2370 drop(reservation);
2371 drop(allocation);
2372 assert_eq!(reserving_manager.allocated_bytes(), 0);
2373 }
2374
2375 #[test]
2376 fn global_budget_rejects_complete_manifest_before_first_inner_allocation() {
2377 let Some((device, runtime, sink)) = try_runtime_with_logging_budget(4095) else {
2378 return;
2379 };
2380 let manager = Arc::new(GpuMemoryManager::with_runtime(
2381 device,
2382 MemoryBudget::with_limit(8192),
2383 runtime,
2384 ));
2385
2386 let error = manager
2387 .reserve_bytes(4096)
2388 .expect_err("the complete request is one byte above the runtime budget");
2389 assert_memory_pressure(
2390 error,
2391 "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",
2392 4096,
2393 4095,
2394 );
2395 assert!(sink.snapshot().is_empty());
2396 assert_eq!(manager.allocated_bytes(), 0);
2397 assert_eq!(manager.remaining_bytes(), 8192);
2398 }
2399
2400 #[test]
2401 fn runtime_backed_reservation_requires_a_reservable_global_budget() {
2402 let Some((device, runtime)) = try_unbudgeted_runtime() else {
2403 return;
2404 };
2405 let manager = Arc::new(GpuMemoryManager::with_runtime(
2406 device,
2407 MemoryBudget::with_limit(4096),
2408 runtime,
2409 ));
2410
2411 let error = manager
2412 .reserve_bytes(1024)
2413 .expect_err("an unbudgeted runtime cannot promise complete admission");
2414 assert!(
2415 matches!(error, XlogError::Kernel(ref detail) if detail.contains("reservable global budget")),
2416 "unexpected error: {error}"
2417 );
2418 assert_eq!(manager.allocated_bytes(), 0);
2419 assert_eq!(manager.remaining_bytes(), 4096);
2420 }
2421
2422 #[test]
2423 fn reservation_materializes_exactly_its_declared_bytes() {
2424 let Some(device) = try_device() else {
2425 return;
2426 };
2427 let manager = Arc::new(GpuMemoryManager::new(
2428 device,
2429 MemoryBudget::with_limit(4096),
2430 ));
2431 let mut reservation = manager
2432 .reserve_bytes(4096)
2433 .expect("the exact complete request must fit");
2434 assert_eq!(reservation.total_bytes(), 4096);
2435 assert_eq!(reservation.remaining_bytes(), 4096);
2436 assert_eq!(reservation.used_bytes(), 0);
2437 assert_eq!(manager.remaining_bytes(), 0);
2438
2439 let words = reservation
2440 .alloc::<u32>(512)
2441 .expect("first reserved allocation");
2442 let bytes = reservation
2443 .alloc::<u8>(2048)
2444 .expect("second reserved allocation");
2445 assert_eq!(reservation.remaining_bytes(), 0);
2446 assert_eq!(reservation.used_bytes(), 4096);
2447 assert_eq!(manager.allocated_bytes(), 4096);
2448 assert_eq!(manager.peak_bytes(), 4096);
2449
2450 let error = match reservation.alloc::<u8>(1) {
2451 Err(error) => error,
2452 Ok(_) => panic!("a reservation cannot materialize more than its declaration"),
2453 };
2454 assert_memory_pressure(
2455 error,
2456 "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",
2457 4097,
2458 4096,
2459 );
2460
2461 drop(reservation);
2462 assert_eq!(manager.remaining_bytes(), 0);
2463 drop(bytes);
2464 assert_eq!(manager.remaining_bytes(), 2048);
2465 drop(words);
2466 assert_eq!(manager.allocated_bytes(), 0);
2467 assert_eq!(manager.remaining_bytes(), 4096);
2468 }
2469
2470 #[test]
2471 fn failed_reserved_raw_allocation_preserves_unused_claim_until_drop() {
2472 let Some((device, runtime)) = try_runtime_with_first_allocation_failure(4096) else {
2473 return;
2474 };
2475 let manager = Arc::new(GpuMemoryManager::with_runtime(
2476 device,
2477 MemoryBudget::with_limit(4096),
2478 runtime,
2479 ));
2480 let mut reservation = manager
2481 .reserve_bytes(4096)
2482 .expect("local complete request must fit");
2483
2484 let error = reservation
2485 .alloc_raw(2048, AllocTag::UNTAGGED)
2486 .expect_err("the injected underlying allocation must fail");
2487 assert!(
2488 matches!(error, XlogError::Kernel(ref detail) if detail.contains("injected allocation failure")),
2489 "unexpected error: {error}"
2490 );
2491 assert_eq!(reservation.remaining_bytes(), 4096);
2492 assert_eq!(manager.allocated_bytes(), 0);
2493 assert_eq!(manager.remaining_bytes(), 0);
2494
2495 let allocation = reservation
2496 .alloc_raw(1024, AllocTag::UNTAGGED)
2497 .expect("a smaller raw suballocation must fit both budgets");
2498 assert_eq!(reservation.remaining_bytes(), 3072);
2499 assert_eq!(manager.allocated_bytes(), 1024);
2500
2501 drop(reservation);
2502 assert_eq!(manager.remaining_bytes(), 3072);
2503 drop(allocation);
2504 assert_eq!(manager.allocated_bytes(), 0);
2505 assert_eq!(manager.remaining_bytes(), 4096);
2506 }
2507
2508 #[test]
2509 fn partial_typed_materialization_releases_each_byte_once_in_either_drop_order() {
2510 let Some(device) = try_device() else {
2511 return;
2512 };
2513 let manager = Arc::new(GpuMemoryManager::new(
2514 device,
2515 MemoryBudget::with_limit(4096),
2516 ));
2517 let mut reservation = manager.reserve_bytes(4096).expect("complete request");
2518 let allocation = reservation
2519 .alloc::<u8>(1024)
2520 .expect("partial materialization");
2521 let error = match reservation.alloc::<u8>(4096) {
2522 Err(error) => error,
2523 Ok(_) => panic!("the remaining reservation is only 3072 bytes"),
2524 };
2525 assert_memory_pressure(
2526 error,
2527 "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",
2528 5120,
2529 4096,
2530 );
2531 assert_eq!(reservation.remaining_bytes(), 3072);
2532
2533 drop(allocation);
2534 assert_eq!(manager.allocated_bytes(), 0);
2535 assert_eq!(manager.remaining_bytes(), 1024);
2536 drop(reservation);
2537 assert_eq!(manager.remaining_bytes(), 4096);
2538 }
2539
2540 #[test]
2541 fn ordinary_allocations_and_complete_reservations_share_one_budget() {
2542 let Some(device) = try_device() else {
2543 return;
2544 };
2545 let manager = Arc::new(GpuMemoryManager::new(
2546 device,
2547 MemoryBudget::with_limit(4096),
2548 ));
2549 let ordinary = manager
2550 .alloc::<u8>(1024)
2551 .expect("ordinary allocation before reservation");
2552 let mut reservation = manager
2553 .reserve_bytes(3072)
2554 .expect("reservation must fit beside the ordinary owner");
2555 assert_eq!(manager.remaining_bytes(), 0);
2556 assert!(matches!(
2557 manager.alloc::<u8>(1),
2558 Err(XlogError::ResourceExhausted { .. })
2559 ));
2560
2561 let reserved = reservation
2562 .alloc::<u8>(3072)
2563 .expect("reserved allocation bypasses the already-paid local claim");
2564 assert_eq!(manager.allocated_bytes(), 4096);
2565 drop(reservation);
2566 drop(reserved);
2567 assert_eq!(manager.allocated_bytes(), 1024);
2568 assert_eq!(manager.remaining_bytes(), 3072);
2569 drop(ordinary);
2570 assert_eq!(manager.allocated_bytes(), 0);
2571 assert_eq!(manager.remaining_bytes(), 4096);
2572 }
2573
2574 #[test]
2575 fn public_accounting_mutators_refuse_live_reservations_and_allocations() {
2576 let Some(device) = try_device() else {
2577 return;
2578 };
2579 let manager = Arc::new(GpuMemoryManager::new(
2580 device,
2581 MemoryBudget::with_limit(4096),
2582 ));
2583 let reservation = manager.reserve_bytes(2048).expect("reservation");
2584
2585 let free_error = manager
2586 .record_free(1)
2587 .expect_err("public accounting cannot release a live reservation");
2588 assert!(
2589 matches!(free_error, XlogError::Kernel(detail) if detail.contains("live tracked bytes"))
2590 );
2591 let reset_error = manager
2592 .reset_tracking()
2593 .expect_err("public reset cannot erase a live reservation");
2594 assert!(
2595 matches!(reset_error, XlogError::Kernel(detail) if detail.contains("live tracked bytes"))
2596 );
2597 assert_eq!(manager.remaining_bytes(), 2048);
2598
2599 drop(reservation);
2600 let allocation = manager.alloc::<u8>(1024).expect("allocation");
2601 assert!(manager.record_free(1024).is_err());
2602 assert!(manager.reset_tracking().is_err());
2603 assert_eq!(manager.allocated_bytes(), 1024);
2604 assert_eq!(manager.remaining_bytes(), 3072);
2605
2606 drop(allocation);
2607 manager
2608 .reset_tracking()
2609 .expect("quiescent accounting can reset diagnostic peaks");
2610 assert_eq!(manager.allocated_bytes(), 0);
2611 assert_eq!(manager.remaining_bytes(), 4096);
2612 }
2613
2614 #[test]
2615 fn public_record_free_refuses_underflow_without_mutation() {
2616 let Some(device) = try_device() else {
2617 return;
2618 };
2619 let manager = Arc::new(GpuMemoryManager::new(
2620 device,
2621 MemoryBudget::with_limit(4096),
2622 ));
2623
2624 let error = manager
2625 .record_free(1)
2626 .expect_err("unowned bytes cannot be released from accounting");
2627 assert!(matches!(error, XlogError::Kernel(detail) if detail.contains("underflow")));
2628 assert_eq!(manager.allocated_bytes(), 0);
2629 assert_eq!(manager.remaining_bytes(), 4096);
2630 manager
2631 .record_free(0)
2632 .expect("zero-byte release is a no-op");
2633 }
2634
2635 #[test]
2636 fn typed_deallocation_failure_retains_local_charge_and_records_failure() {
2637 let Some((device, runtime, deallocate_calls)) = try_runtime_with_deallocation_failure()
2638 else {
2639 return;
2640 };
2641 let manager = Arc::new(GpuMemoryManager::with_runtime(
2642 device,
2643 MemoryBudget::with_limit(4096),
2644 Arc::clone(&runtime),
2645 ));
2646 let allocation = manager.alloc::<u8>(1024).expect("typed allocation");
2647
2648 drop(allocation);
2649
2650 assert_eq!(deallocate_calls.load(Ordering::SeqCst), 1);
2651 assert_eq!(runtime.bytes_outstanding(), 0);
2652 assert_eq!(manager.allocated_bytes(), 1024);
2653 assert_eq!(manager.remaining_bytes(), 3072);
2654 assert_eq!(manager.deallocation_failure_count(), 1);
2655 assert_eq!(manager.deallocation_failure_bytes(), 1024);
2656 assert!(manager.reset_tracking().is_err());
2657 }
2658
2659 #[test]
2660 fn raw_deallocation_failure_retains_local_charge_and_records_failure() {
2661 let Some((device, runtime, deallocate_calls)) = try_runtime_with_deallocation_failure()
2662 else {
2663 return;
2664 };
2665 let manager = Arc::new(GpuMemoryManager::with_runtime(
2666 device,
2667 MemoryBudget::with_limit(4096),
2668 Arc::clone(&runtime),
2669 ));
2670 let allocation = manager
2671 .alloc_raw(512, AllocTag::UNTAGGED)
2672 .expect("raw allocation");
2673
2674 drop(allocation);
2675
2676 assert_eq!(deallocate_calls.load(Ordering::SeqCst), 1);
2677 assert_eq!(runtime.bytes_outstanding(), 0);
2678 assert_eq!(manager.allocated_bytes(), 512);
2679 assert_eq!(manager.remaining_bytes(), 3584);
2680 assert_eq!(manager.deallocation_failure_count(), 1);
2681 assert_eq!(manager.deallocation_failure_bytes(), 512);
2682 assert!(manager.reset_tracking().is_err());
2683 }
2684
2685 #[test]
2686 fn test_memory_manager_multiple_allocs() {
2687 let Some(device) = try_device() else {
2688 return;
2689 };
2690 let budget = MemoryBudget::with_limit(4096); let manager = Arc::new(GpuMemoryManager::new(device, budget));
2692
2693 let _slice1 = manager
2695 .alloc::<u32>(256)
2696 .expect("First allocation should succeed");
2697 assert_eq!(manager.allocated_bytes(), 1024);
2698
2699 let _slice2 = manager
2701 .alloc::<u32>(256)
2702 .expect("Second allocation should succeed");
2703 assert_eq!(manager.allocated_bytes(), 2048);
2704
2705 let result = manager.alloc::<u32>(1024); assert!(result.is_err());
2708
2709 assert_eq!(manager.allocated_bytes(), 2048);
2711 }
2712
2713 #[test]
2714 fn test_memory_manager_record_free() {
2715 let Some(device) = try_device() else {
2716 return;
2717 };
2718 let budget = MemoryBudget::with_limit(4096);
2719 let manager = Arc::new(GpuMemoryManager::new(device, budget));
2720
2721 let slice = manager
2723 .alloc::<u32>(256)
2724 .expect("Allocation should succeed");
2725 assert_eq!(manager.allocated_bytes(), 1024);
2726
2727 drop(slice);
2729 assert_eq!(manager.allocated_bytes(), 0);
2730 assert_eq!(manager.remaining_bytes(), 4096);
2731 }
2732
2733 #[test]
2734 fn test_memory_manager_peak_tracking() {
2735 let Some(device) = try_device() else {
2736 return;
2737 };
2738 let budget = MemoryBudget::with_limit(8192);
2739 let manager = Arc::new(GpuMemoryManager::new(device, budget));
2740
2741 let a = manager.alloc::<u32>(256).expect("alloc a"); let b = manager.alloc::<u32>(512).expect("alloc b"); assert_eq!(manager.peak_bytes(), 3072);
2744
2745 drop(b);
2747 assert_eq!(manager.allocated_bytes(), 1024);
2748 assert_eq!(manager.peak_bytes(), 3072);
2749
2750 manager.reset_peak();
2752 assert_eq!(manager.peak_bytes(), 1024);
2753
2754 let c = manager.alloc::<u32>(128).expect("alloc c"); assert_eq!(manager.peak_bytes(), 1536);
2756
2757 drop(c);
2758 drop(a);
2759 assert_eq!(manager.allocated_bytes(), 0);
2760 assert_eq!(manager.peak_bytes(), 1536);
2761 }
2762
2763 #[test]
2764 fn memory_pressure_alloc_reports_exact_cumulative_pressure() {
2765 let Some(device) = try_device() else {
2766 return;
2767 };
2768 let manager = Arc::new(GpuMemoryManager::new(
2769 device,
2770 MemoryBudget::with_limit(4096),
2771 ));
2772 let baseline = manager.alloc::<u8>(1024).expect("baseline allocation");
2773
2774 let error = match manager.alloc::<u8>(4096) {
2775 Err(error) => error,
2776 Ok(_) => panic!("cumulative allocation must exceed the local budget"),
2777 };
2778
2779 assert_memory_pressure(
2780 error,
2781 "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",
2782 5120,
2783 4096,
2784 );
2785 assert_eq!(manager.allocated_bytes(), 1024);
2786 assert_eq!(manager.peak_bytes(), 1024, "refusal must not raise peak");
2787 drop(baseline);
2788 }
2789
2790 #[test]
2791 fn memory_pressure_check_budget_reports_exact_cumulative_pressure() {
2792 let Some(device) = try_device() else {
2793 return;
2794 };
2795 let manager = Arc::new(GpuMemoryManager::new(
2796 device,
2797 MemoryBudget::with_limit(1000),
2798 ));
2799 let baseline = manager.alloc::<u8>(512).expect("baseline allocation");
2800
2801 let error = manager
2802 .check_budget(600)
2803 .expect_err("cumulative request must exceed the local budget");
2804
2805 assert_memory_pressure(
2806 error,
2807 "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",
2808 1112,
2809 1000,
2810 );
2811 assert_eq!(manager.allocated_bytes(), 512);
2812 assert_eq!(
2813 manager.peak_bytes(),
2814 512,
2815 "check-only refusal must not raise peak"
2816 );
2817 drop(baseline);
2818 }
2819
2820 #[test]
2821 fn memory_pressure_check_budget_reports_u64_representability_overflow() {
2822 let Some(device) = try_device() else {
2823 return;
2824 };
2825 let manager = Arc::new(GpuMemoryManager::new(
2826 device,
2827 MemoryBudget::with_limit(u64::MAX),
2828 ));
2829 manager
2830 .accounting
2831 .budget_reserved
2832 .store(u64::MAX - 3, Ordering::SeqCst);
2833 manager
2834 .accounting
2835 .allocated
2836 .store(u64::MAX - 3, Ordering::SeqCst);
2837 manager
2838 .accounting
2839 .peak
2840 .store(u64::MAX - 3, Ordering::SeqCst);
2841
2842 let error = manager
2843 .check_budget(8)
2844 .expect_err("the exact required byte count is not representable as u64");
2845
2846 assert_memory_pressure(
2847 error,
2848 "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",
2849 u64::MAX,
2850 u64::MAX,
2851 );
2852 assert_eq!(manager.allocated_bytes(), u64::MAX - 3);
2853 assert_eq!(manager.peak_bytes(), u64::MAX - 3);
2854
2855 manager
2857 .accounting
2858 .budget_reserved
2859 .store(0, Ordering::SeqCst);
2860 manager.accounting.allocated.store(0, Ordering::SeqCst);
2861 manager.accounting.peak.store(0, Ordering::SeqCst);
2862 }
2863
2864 #[test]
2865 fn test_cuda_buffer_from_columns() {
2866 let Some(device) = try_device() else {
2867 return;
2868 };
2869 let budget = MemoryBudget::with_limit(1024 * 1024);
2870 let manager = Arc::new(GpuMemoryManager::new(device, budget));
2871
2872 let schema = Schema::new(vec![
2873 ("col1".to_string(), ScalarType::U32),
2874 ("col2".to_string(), ScalarType::U32),
2875 ]);
2876
2877 let col1 = manager.alloc::<u8>(400).expect("Alloc col1");
2879 let col2 = manager.alloc::<u8>(400).expect("Alloc col2");
2880
2881 let mut d_num_rows = manager.alloc::<u32>(1).expect("Alloc row count");
2882 manager
2883 .device()
2884 .inner()
2885 .htod_sync_copy_into(&[100u32], &mut d_num_rows)
2886 .expect("Upload row count");
2887 let buffer =
2888 CudaBuffer::from_columns(vec![col1.into(), col2.into()], 100, d_num_rows, schema);
2889
2890 assert_eq!(buffer.num_rows(), 100);
2891 assert_eq!(buffer.arity(), 2);
2892 assert!(!buffer.is_empty());
2893 assert!(buffer.column(0).is_some());
2894 assert!(buffer.column(1).is_some());
2895 assert!(buffer.column(2).is_none());
2896 }
2897
2898 #[test]
2899 fn test_cuda_buffer_from_columns_mismatch() {
2900 let schema = Schema::new(vec![
2901 ("col1".to_string(), ScalarType::U32),
2902 ("col2".to_string(), ScalarType::U32),
2903 ]);
2904
2905 let Some(device) = try_device() else {
2906 return;
2907 };
2908 let budget = MemoryBudget::with_limit(1024 * 1024);
2909 let manager = Arc::new(GpuMemoryManager::new(device, budget));
2910 let mut d_num_rows = manager.alloc::<u32>(1).expect("Alloc row count");
2911 manager
2912 .device()
2913 .inner()
2914 .htod_sync_copy_into(&[100u32], &mut d_num_rows)
2915 .expect("Upload row count");
2916
2917 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2919 CudaBuffer::from_columns(vec![], 100, d_num_rows, schema);
2920 }));
2921 assert!(
2922 result.is_err(),
2923 "Expected from_columns to panic on schema mismatch"
2924 );
2925 }
2926
2927 fn try_runtime() -> Option<(
2928 Arc<CudaDevice>,
2929 Arc<crate::device_runtime::XlogDeviceRuntime>,
2930 )> {
2931 use crate::device_runtime::{
2932 AsyncCudaResource, DeviceMemoryResource, GlobalDeviceBudget, StreamPool,
2933 XlogDeviceRuntime,
2934 };
2935 let device = try_device()?;
2936 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
2937 let async_resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
2938 AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
2939 );
2940 let budget: Box<dyn DeviceMemoryResource + Send + Sync> =
2941 Box::new(GlobalDeviceBudget::new(async_resource, 64 * 1024 * 1024));
2942 Some((
2943 Arc::clone(&device),
2944 Arc::new(XlogDeviceRuntime::with_resource(
2945 Arc::clone(&device),
2946 0,
2947 pool,
2948 budget,
2949 )),
2950 ))
2951 }
2952
2953 fn try_unbudgeted_runtime() -> Option<(
2954 Arc<CudaDevice>,
2955 Arc<crate::device_runtime::XlogDeviceRuntime>,
2956 )> {
2957 use crate::device_runtime::{
2958 AsyncCudaResource, DeviceMemoryResource, StreamPool, XlogDeviceRuntime,
2959 };
2960 let device = try_device()?;
2961 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
2962 let resource: Box<dyn DeviceMemoryResource + Send + Sync> = Box::new(
2963 AsyncCudaResource::new(Arc::clone(&device), 0, Arc::clone(&pool)),
2964 );
2965 Some((
2966 Arc::clone(&device),
2967 Arc::new(XlogDeviceRuntime::with_resource(
2968 Arc::clone(&device),
2969 0,
2970 pool,
2971 resource,
2972 )),
2973 ))
2974 }
2975
2976 fn try_runtime_with_deallocation_failure() -> Option<(
2977 Arc<CudaDevice>,
2978 Arc<crate::device_runtime::XlogDeviceRuntime>,
2979 Arc<AtomicU64>,
2980 )> {
2981 use crate::device_runtime::{StreamPool, XlogDeviceRuntime};
2982 let device = try_device()?;
2983 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
2984 let deallocate_calls = Arc::new(AtomicU64::new(0));
2985 let resource: Box<dyn DeviceMemoryResource + Send + Sync> =
2986 Box::new(FailAfterDeallocateResource {
2987 inner: DirectCudaResource::new(Arc::clone(&device), 0),
2988 deallocate_calls: Arc::clone(&deallocate_calls),
2989 });
2990 Some((
2991 Arc::clone(&device),
2992 Arc::new(XlogDeviceRuntime::with_resource(
2993 Arc::clone(&device),
2994 0,
2995 pool,
2996 resource,
2997 )),
2998 deallocate_calls,
2999 ))
3000 }
3001
3002 fn try_runtime_with_first_allocation_failure(
3003 limit: usize,
3004 ) -> Option<(
3005 Arc<CudaDevice>,
3006 Arc<crate::device_runtime::XlogDeviceRuntime>,
3007 )> {
3008 use crate::device_runtime::{GlobalDeviceBudget, StreamPool, XlogDeviceRuntime};
3009 let device = try_device()?;
3010 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
3011 let failing_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
3012 Box::new(FailFirstAllocationResource {
3013 inner: DirectCudaResource::new(Arc::clone(&device), 0),
3014 fail_next: std::sync::atomic::AtomicBool::new(true),
3015 });
3016 let budget_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
3017 Box::new(GlobalDeviceBudget::new(failing_resource, limit));
3018 Some((
3019 Arc::clone(&device),
3020 Arc::new(XlogDeviceRuntime::with_resource(
3021 Arc::clone(&device),
3022 0,
3023 pool,
3024 budget_resource,
3025 )),
3026 ))
3027 }
3028
3029 fn try_runtime_with_budget(
3030 limit: usize,
3031 ) -> Option<(
3032 Arc<CudaDevice>,
3033 Arc<crate::device_runtime::XlogDeviceRuntime>,
3034 )> {
3035 use crate::device_runtime::{
3036 DeviceMemoryResource, DirectCudaResource, GlobalDeviceBudget, StreamPool,
3037 XlogDeviceRuntime,
3038 };
3039 let device = try_device()?;
3040 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
3041 let direct_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
3042 Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
3043 let budget_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
3044 Box::new(GlobalDeviceBudget::new(direct_resource, limit));
3045 Some((
3046 Arc::clone(&device),
3047 Arc::new(XlogDeviceRuntime::with_resource(
3048 Arc::clone(&device),
3049 0,
3050 pool,
3051 budget_resource,
3052 )),
3053 ))
3054 }
3055
3056 fn try_runtime_with_logging_budget(
3057 limit: usize,
3058 ) -> Option<(
3059 Arc<CudaDevice>,
3060 Arc<crate::device_runtime::XlogDeviceRuntime>,
3061 Arc<crate::device_runtime::InMemorySink>,
3062 )> {
3063 use crate::device_runtime::{
3064 DeviceMemoryResource, DirectCudaResource, GlobalDeviceBudget, InMemorySink,
3065 LoggingResource, LoggingSink, StreamPool, XlogDeviceRuntime,
3066 };
3067 let device = try_device()?;
3068 let pool = Arc::new(StreamPool::with_defaults(Arc::clone(&device)));
3069 let direct_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
3070 Box::new(DirectCudaResource::new(Arc::clone(&device), 0));
3071 let budget_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
3072 Box::new(GlobalDeviceBudget::new(direct_resource, limit));
3073 let sink = Arc::new(InMemorySink::new());
3074 let logging_sink: Arc<dyn LoggingSink> = sink.clone();
3075 let logging_resource: Box<dyn DeviceMemoryResource + Send + Sync> =
3076 Box::new(LoggingResource::new(budget_resource, logging_sink));
3077 Some((
3078 Arc::clone(&device),
3079 Arc::new(XlogDeviceRuntime::with_resource(
3080 Arc::clone(&device),
3081 0,
3082 pool,
3083 logging_resource,
3084 )),
3085 sink,
3086 ))
3087 }
3088
3089 fn pause_request_after_local_reservation(
3090 manager: &GpuMemoryManager,
3091 paused_bytes: u64,
3092 ) -> (Arc<std::sync::Barrier>, Arc<std::sync::Barrier>) {
3093 let entered = Arc::new(std::sync::Barrier::new(2));
3094 let release = Arc::new(std::sync::Barrier::new(2));
3095 let hook_entered = Arc::clone(&entered);
3096 let hook_release = Arc::clone(&release);
3097 *manager
3098 .after_local_reservation_hook
3099 .lock()
3100 .expect("after-local-reservation test hook poisoned") = Some(Arc::new(move |bytes| {
3101 if bytes == paused_bytes {
3102 hook_entered.wait();
3103 hook_release.wait();
3104 }
3105 }));
3106 (entered, release)
3107 }
3108
3109 #[test]
3110 fn memory_pressure_concurrent_alloc_raw_refusal_excludes_provisional_bytes() {
3111 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
3112 return;
3113 };
3114 let manager = Arc::new(GpuMemoryManager::with_runtime(
3115 device,
3116 MemoryBudget::with_limit(8192),
3117 runtime,
3118 ));
3119 let (paused, release) = pause_request_after_local_reservation(&manager, 4096);
3120
3121 let refused_manager = Arc::clone(&manager);
3122 let refused = std::thread::spawn(move || {
3123 refused_manager
3124 .alloc_raw(4096, AllocTag::UNTAGGED)
3125 .expect_err("runtime must refuse the paused request")
3126 });
3127 paused.wait();
3128
3129 let admitted = manager
3130 .alloc_raw(1024, AllocTag::UNTAGGED)
3131 .expect("concurrent smaller request must be admitted");
3132 release.wait();
3133 let error = refused.join().expect("paused allocation thread panicked");
3134
3135 assert_memory_pressure(
3136 error,
3137 "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",
3138 5120,
3139 4096,
3140 );
3141 assert_eq!(manager.allocated_bytes(), 1024);
3142 assert_eq!(
3143 manager.peak_bytes(),
3144 1024,
3145 "refused provisional bytes must never enter the admitted peak"
3146 );
3147 drop(admitted);
3148 }
3149
3150 #[test]
3151 fn memory_pressure_concurrent_typed_alloc_refusal_excludes_provisional_bytes() {
3152 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
3153 return;
3154 };
3155 let manager = Arc::new(GpuMemoryManager::with_runtime(
3156 device,
3157 MemoryBudget::with_limit(8192),
3158 runtime,
3159 ));
3160 let (paused, release) = pause_request_after_local_reservation(&manager, 4096);
3161
3162 let refused_manager = Arc::clone(&manager);
3163 let refused = std::thread::spawn(move || match refused_manager.alloc::<u8>(4096) {
3164 Err(error) => error,
3165 Ok(_) => panic!("runtime must refuse the paused typed request"),
3166 });
3167 paused.wait();
3168
3169 let admitted = manager
3170 .alloc::<u8>(1024)
3171 .expect("concurrent smaller typed request must be admitted");
3172 release.wait();
3173 let error = refused.join().expect("paused allocation thread panicked");
3174
3175 assert_memory_pressure(
3176 error,
3177 "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",
3178 5120,
3179 4096,
3180 );
3181 assert_eq!(manager.allocated_bytes(), 1024);
3182 assert_eq!(
3183 manager.peak_bytes(),
3184 1024,
3185 "refused provisional bytes must never enter the admitted peak"
3186 );
3187 drop(admitted);
3188 }
3189
3190 #[test]
3191 fn memory_pressure_alloc_raw_reports_exact_local_pressure() {
3192 let Some((device, runtime)) = try_runtime_with_budget(64 * 1024) else {
3193 return;
3194 };
3195 let manager = Arc::new(GpuMemoryManager::with_runtime(
3196 device,
3197 MemoryBudget::with_limit(4096),
3198 runtime,
3199 ));
3200 let baseline = manager
3201 .alloc_raw(1024, AllocTag::UNTAGGED)
3202 .expect("baseline allocation");
3203
3204 let error = manager
3205 .alloc_raw(4096, AllocTag::UNTAGGED)
3206 .expect_err("cumulative allocation must exceed the local budget");
3207
3208 assert_memory_pressure(
3209 error,
3210 "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",
3211 5120,
3212 4096,
3213 );
3214 assert_eq!(manager.allocated_bytes(), 1024);
3215 assert_eq!(manager.peak_bytes(), 1024, "refusal must not raise peak");
3216 drop(baseline);
3217 }
3218
3219 #[test]
3220 fn memory_pressure_alloc_raw_drop_restores_local_headroom() {
3221 let Some((device, runtime)) = try_runtime_with_budget(64 * 1024) else {
3222 return;
3223 };
3224 let manager = Arc::new(GpuMemoryManager::with_runtime(
3225 device,
3226 MemoryBudget::with_limit(1024),
3227 runtime,
3228 ));
3229
3230 let allocation = manager
3231 .alloc_raw(1024, AllocTag::UNTAGGED)
3232 .expect("initial allocation must consume the local budget");
3233 assert_eq!(manager.allocated_bytes(), 1024);
3234 assert_eq!(manager.remaining_bytes(), 0);
3235
3236 drop(allocation);
3237 assert_eq!(manager.allocated_bytes(), 0);
3238 assert_eq!(
3239 manager.remaining_bytes(),
3240 1024,
3241 "dropping an admitted raw allocation must restore local headroom"
3242 );
3243
3244 let replacement = manager
3245 .alloc_raw(1024, AllocTag::UNTAGGED)
3246 .expect("restored local headroom must admit a replacement allocation");
3247 drop(replacement);
3248 }
3249
3250 #[test]
3251 fn memory_pressure_runtime_rejection_preserves_peak() {
3252 let Some((device, runtime)) = try_runtime_with_budget(4096) else {
3253 return;
3254 };
3255 let manager = Arc::new(GpuMemoryManager::with_runtime(
3256 device,
3257 MemoryBudget::with_limit(8192),
3258 runtime,
3259 ));
3260 let baseline = manager
3261 .alloc_raw(1024, AllocTag::UNTAGGED)
3262 .expect("baseline allocation");
3263
3264 let error = manager
3265 .alloc_raw(4096, AllocTag::UNTAGGED)
3266 .expect_err("runtime budget must reject the cumulative allocation");
3267
3268 assert_memory_pressure(
3269 error,
3270 "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",
3271 5120,
3272 4096,
3273 );
3274 assert_eq!(
3275 manager.allocated_bytes(),
3276 1024,
3277 "local reservation must roll back"
3278 );
3279 assert_eq!(
3280 manager.peak_bytes(),
3281 1024,
3282 "runtime refusal must not advance the manager peak"
3283 );
3284 drop(baseline);
3285 }
3286
3287 #[test]
3297 fn test_xlog_owned_dlpack_runtime_backed_carries_identity() {
3298 let Some((device, runtime)) = try_runtime() else {
3299 return;
3300 };
3301 let manager = Arc::new(GpuMemoryManager::with_runtime(
3302 Arc::clone(&device),
3303 MemoryBudget::with_limit(1024 * 1024),
3304 Arc::clone(&runtime),
3305 ));
3306 let slice = manager.alloc::<u8>(64).expect("alloc runtime-backed");
3307 assert!(slice.runtime_block().is_some());
3308 let stream = device.inner().stream().clone();
3309 let tensor = unsafe { DlpackManagedTensor::from_raw(std::ptr::null_mut()) };
3314 let col = CudaColumn::dlpack_xlog_owned(Arc::new(slice), stream, tensor);
3315 assert!(
3316 !col.is_external(),
3317 "xlog-owned DLPack column must report is_external=false"
3318 );
3319 assert!(
3320 col.runtime_block().is_some(),
3321 "xlog-owned DLPack column over a runtime-backed slice must expose runtime_block"
3322 );
3323 }
3324
3325 #[test]
3332 fn test_xlog_owned_dlpack_legacy_backed_no_runtime_block() {
3333 let Some(device) = try_device() else {
3334 return;
3335 };
3336 let manager = Arc::new(GpuMemoryManager::new(
3337 Arc::clone(&device),
3338 MemoryBudget::with_limit(1024 * 1024),
3339 ));
3340 let slice = manager.alloc::<u8>(64).expect("alloc legacy");
3341 assert!(slice.runtime_block().is_none());
3342 let stream = device.inner().stream().clone();
3343 let tensor = unsafe { DlpackManagedTensor::from_raw(std::ptr::null_mut()) };
3344 let col = CudaColumn::dlpack_xlog_owned(Arc::new(slice), stream, tensor);
3345 assert!(
3346 !col.is_external(),
3347 "xlog-owned DLPack column is owned regardless of allocator backing"
3348 );
3349 assert!(
3350 col.runtime_block().is_none(),
3351 "legacy-backed slice has no runtime block, even when wrapped xlog-owned"
3352 );
3353 }
3354
3355 #[test]
3360 fn test_external_dlpack_remains_external() {
3361 let Some(device) = try_device() else {
3362 return;
3363 };
3364 let stream = device.inner().stream().clone();
3365 let tensor = unsafe { DlpackManagedTensor::from_raw(std::ptr::null_mut()) };
3366 let col = CudaColumn::dlpack(0, 0, stream, tensor);
3369 assert!(
3370 col.is_external(),
3371 "true external DLPack column must report is_external=true"
3372 );
3373 assert!(
3374 col.runtime_block().is_none(),
3375 "true external DLPack column has no xlog-side runtime block"
3376 );
3377 }
3378
3379 #[test]
3384 fn test_xlog_owned_arrow_device_runtime_backed_carries_identity() {
3385 let Some((device, runtime)) = try_runtime() else {
3386 return;
3387 };
3388 let manager = Arc::new(GpuMemoryManager::with_runtime(
3389 Arc::clone(&device),
3390 MemoryBudget::with_limit(1024 * 1024),
3391 Arc::clone(&runtime),
3392 ));
3393 let slice = manager.alloc::<u8>(64).expect("alloc runtime-backed");
3394 assert!(slice.runtime_block().is_some());
3395 let stream = device.inner().stream().clone();
3396 let import = Arc::new(crate::arrow_device::ArrowDeviceImport::new(
3400 arrow::array::ArrayData::new_null(&arrow::datatypes::DataType::UInt8, 0),
3401 ));
3402 let col = CudaColumn::arrow_device_xlog_owned(Arc::new(slice), stream, import);
3403 assert!(
3404 !col.is_external(),
3405 "xlog-owned Arrow device column must report is_external=false"
3406 );
3407 assert!(
3408 col.runtime_block().is_some(),
3409 "xlog-owned Arrow column over a runtime-backed slice must expose runtime_block"
3410 );
3411 }
3412
3413 #[test]
3416 fn test_external_arrow_device_remains_external() {
3417 let Some(device) = try_device() else {
3418 return;
3419 };
3420 let stream = device.inner().stream().clone();
3421 let import = Arc::new(crate::arrow_device::ArrowDeviceImport::new(
3422 arrow::array::ArrayData::new_null(&arrow::datatypes::DataType::UInt8, 0),
3423 ));
3424 let col = CudaColumn::arrow_device(0, 0, stream, import);
3425 assert!(
3426 col.is_external(),
3427 "true external Arrow column must report is_external=true"
3428 );
3429 assert!(
3430 col.runtime_block().is_none(),
3431 "true external Arrow column has no xlog-side runtime block"
3432 );
3433 }
3434}