scirs2_fft/sparse_fft_gpu_memory.rs
1//! Memory management for GPU-accelerated sparse FFT
2//!
3//! This module provides memory management utilities for GPU-accelerated sparse FFT
4//! implementations, including buffer allocation, reuse, and transfer optimization.
5
6use crate::error::{FFTError, FFTResult};
7use crate::sparse_fft_gpu::GPUBackend;
8use scirs2_core::numeric::Complex64;
9use scirs2_core::numeric::NumCast;
10use std::collections::HashMap;
11use std::fmt::Debug;
12use std::sync::{Arc, Mutex};
13
14// CUDA support temporarily disabled until cudarc dependency is enabled
15// #[cfg(feature = "cuda")]
16// use cudarc::driver::{CudaDevice, DevicePtr, DriverError};
17
18// HIP support temporarily disabled until hiprt dependency is enabled
19// #[cfg(feature = "hip")]
20// use hiprt::{hipDevice_t, hipDeviceptr_t, hipError_t};
21
22#[cfg(any(feature = "cuda", feature = "hip", feature = "sycl"))]
23use std::sync::OnceLock;
24
25// CUDA support temporarily disabled until cudarc dependency is enabled
26#[cfg(feature = "cuda")]
27static CUDA_DEVICE: OnceLock<Option<Arc<u8>>> = OnceLock::new(); // Placeholder type
28
29// HIP support temporarily disabled until hiprt dependency is enabled
30#[cfg(feature = "hip")]
31static HIP_DEVICE: OnceLock<Option<u8>> = OnceLock::new(); // Placeholder type
32
33#[cfg(feature = "sycl")]
34static SYCL_DEVICE: OnceLock<Option<SyclDevice>> = OnceLock::new();
35
36/// Placeholder SYCL device type
37#[cfg(feature = "sycl")]
38#[derive(Debug, Clone)]
39#[allow(dead_code)]
40pub struct SyclDevice {
41 device_id: i32,
42 device_name: String,
43}
44
45/// Placeholder SYCL device pointer type
46#[cfg(feature = "sycl")]
47pub type SyclDevicePtr = *mut std::os::raw::c_void;
48
49/// Memory buffer location
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum BufferLocation {
52 /// Host (CPU) memory
53 Host,
54 /// Device (GPU) memory
55 Device,
56 /// Pinned host memory (page-locked for faster transfers)
57 PinnedHost,
58 /// Unified memory (accessible from both CPU and GPU)
59 Unified,
60}
61
62/// Memory buffer type
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum BufferType {
65 /// Input signal buffer
66 Input,
67 /// Output signal buffer
68 Output,
69 /// Work buffer for intermediate results
70 Work,
71 /// FFT plan buffer
72 Plan,
73}
74
75/// Buffer allocation strategy
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum AllocationStrategy {
78 /// Allocate once, reuse for same sizes
79 CacheBySize,
80 /// Allocate for each operation
81 AlwaysAllocate,
82 /// Preallocate a fixed size buffer
83 Preallocate,
84 /// Use a pool of buffers
85 BufferPool,
86}
87
88/// Memory buffer descriptor
89#[derive(Debug, Clone)]
90pub struct BufferDescriptor {
91 /// Size of the buffer in elements
92 pub size: usize,
93 /// Element size in bytes
94 pub element_size: usize,
95 /// Buffer location
96 pub location: BufferLocation,
97 /// Buffer type
98 pub buffer_type: BufferType,
99 /// Buffer ID
100 pub id: usize,
101 /// GPU backend used for this buffer
102 pub backend: GPUBackend,
103 /// Device memory pointer (for CUDA)
104 #[cfg(feature = "cuda")]
105 cuda_device_ptr: Option<*mut u8>, // Placeholder type for disabled CUDA
106 /// Device memory pointer (for HIP)
107 #[cfg(feature = "hip")]
108 hip_device_ptr: Option<*mut u8>, // Placeholder type for disabled HIP
109 /// Device memory pointer (for SYCL)
110 #[cfg(feature = "sycl")]
111 sycl_device_ptr: Option<SyclDevicePtr>,
112 /// Host memory pointer (for CPU fallback or pinned memory)
113 host_ptr: Option<*mut std::os::raw::c_void>,
114}
115
116// SAFETY: BufferDescriptor manages memory through proper allocation/deallocation
117// Raw pointers are only used within controlled contexts
118unsafe impl Send for BufferDescriptor {}
119unsafe impl Sync for BufferDescriptor {}
120
121/// Initialize CUDA device (call once at startup)
122#[cfg(feature = "cuda")]
123#[allow(dead_code)]
124pub fn init_cuda_device() -> FFTResult<bool> {
125 let device_result = CUDA_DEVICE.get_or_init(|| {
126 // CUDA device initialization temporarily disabled until cudarc dependency is enabled
127 /*
128 match CudaDevice::new(0) {
129 Ok(device) => Some(Arc::new(device)),
130 Err(_) => None,
131 }
132 */
133 None // Placeholder - no CUDA device available
134 });
135
136 Ok(device_result.is_some())
137}
138
139/// Initialize CUDA device (no-op without CUDA feature)
140#[cfg(not(feature = "cuda"))]
141#[allow(dead_code)]
142pub fn init_cuda_device() -> FFTResult<bool> {
143 Ok(false)
144}
145
146/// Initialize HIP device (call once at startup)
147#[cfg(feature = "hip")]
148#[allow(dead_code)]
149pub fn init_hip_device() -> FFTResult<bool> {
150 // HIP support temporarily disabled until hiprt dependency is enabled
151 Err(FFTError::NotImplementedError(
152 "HIP support is temporarily disabled".to_string(),
153 ))
154}
155
156/// Initialize HIP device (no-op without HIP feature)
157#[cfg(not(feature = "hip"))]
158#[allow(dead_code)]
159pub fn init_hip_device() -> FFTResult<bool> {
160 Ok(false)
161}
162
163/// Initialize SYCL device (call once at startup)
164#[cfg(feature = "sycl")]
165#[allow(dead_code)]
166pub fn init_sycl_device() -> FFTResult<bool> {
167 let device_result = SYCL_DEVICE.get_or_init(|| {
168 // In a real SYCL implementation, this would:
169 // 1. Query available SYCL devices
170 // 2. Select the best device (GPU preferred, then CPU)
171 // 3. Create a SYCL context and queue
172
173 // For now, we'll create a placeholder device
174 Some(SyclDevice {
175 device_id: 0,
176 device_name: "Generic SYCL Device".to_string(),
177 })
178 });
179
180 Ok(device_result.is_some())
181}
182
183/// Initialize SYCL device (no-op without SYCL feature)
184#[cfg(not(feature = "sycl"))]
185#[allow(dead_code)]
186pub fn init_sycl_device() -> FFTResult<bool> {
187 Ok(false)
188}
189
190/// Check if CUDA is available
191#[cfg(feature = "cuda")]
192#[allow(dead_code)]
193pub fn is_cuda_available() -> bool {
194 CUDA_DEVICE.get().map(|d| d.is_some()).unwrap_or(false)
195}
196
197/// Check if CUDA is available (always false without CUDA feature)
198#[cfg(not(feature = "cuda"))]
199#[allow(dead_code)]
200pub fn is_cuda_available() -> bool {
201 false
202}
203
204/// Check if HIP is available
205#[cfg(feature = "hip")]
206#[allow(dead_code)]
207pub fn is_hip_available() -> bool {
208 HIP_DEVICE.get().map(|d| d.is_some()).unwrap_or(false)
209}
210
211/// Check if HIP is available (always false without HIP feature)
212#[cfg(not(feature = "hip"))]
213#[allow(dead_code)]
214pub fn is_hip_available() -> bool {
215 false
216}
217
218/// Check if SYCL is available
219#[cfg(feature = "sycl")]
220#[allow(dead_code)]
221pub fn is_sycl_available() -> bool {
222 SYCL_DEVICE.get().map(|d| d.is_some()).unwrap_or(false)
223}
224
225/// Check if SYCL is available (always false without SYCL feature)
226#[cfg(not(feature = "sycl"))]
227#[allow(dead_code)]
228pub fn is_sycl_available() -> bool {
229 false
230}
231
232/// Check if any GPU backend is available
233#[allow(dead_code)]
234pub fn is_gpu_available() -> bool {
235 is_cuda_available() || is_hip_available() || is_sycl_available()
236}
237
238/// Initialize the best available GPU backend
239#[allow(dead_code)]
240pub fn init_gpu_backend() -> FFTResult<GPUBackend> {
241 // Try CUDA first (usually fastest)
242 if init_cuda_device()? {
243 return Ok(GPUBackend::CUDA);
244 }
245
246 // Then try HIP (AMD GPUs)
247 if init_hip_device()? {
248 return Ok(GPUBackend::HIP);
249 }
250
251 // Then try SYCL (cross-platform, Intel GPUs, etc.)
252 if init_sycl_device()? {
253 return Ok(GPUBackend::SYCL);
254 }
255
256 // Fall back to CPU
257 Ok(GPUBackend::CPUFallback)
258}
259
260impl BufferDescriptor {
261 /// Create a new buffer descriptor with specified backend
262 pub fn new(
263 size: usize,
264 element_size: usize,
265 location: BufferLocation,
266 buffer_type: BufferType,
267 id: usize,
268 backend: GPUBackend,
269 ) -> FFTResult<Self> {
270 let mut descriptor = Self {
271 size,
272 element_size,
273 location,
274 buffer_type,
275 id,
276 backend,
277 #[cfg(feature = "cuda")]
278 cuda_device_ptr: None,
279 #[cfg(feature = "hip")]
280 hip_device_ptr: None,
281 #[cfg(feature = "sycl")]
282 sycl_device_ptr: None,
283 host_ptr: None,
284 };
285
286 descriptor.allocate()?;
287 Ok(descriptor)
288 }
289
290 /// Create a new buffer descriptor with auto-detected backend
291 pub fn new_auto(
292 size: usize,
293 element_size: usize,
294 location: BufferLocation,
295 buffer_type: BufferType,
296 id: usize,
297 ) -> FFTResult<Self> {
298 let backend = init_gpu_backend()?;
299 Self::new(size, element_size, location, buffer_type, id, backend)
300 }
301
302 /// Allocate the actual memory based on location and backend
303 fn allocate(&mut self) -> FFTResult<()> {
304 let total_size = self.size * self.element_size;
305
306 match self.location {
307 BufferLocation::Device => {
308 match self.backend {
309 GPUBackend::CUDA => {
310 #[cfg(feature = "cuda")]
311 {
312 if let Some(_device) = CUDA_DEVICE.get().and_then(|d| d.as_ref()) {
313 // CUDA API calls temporarily disabled until cudarc dependency is enabled
314 /*
315 let device_mem = device.alloc::<u8>(total_size).map_err(|e| {
316 FFTError::ComputationError(format!(
317 "Failed to allocate CUDA memory: {:?}",
318 e
319 ))
320 })?;
321 self.cuda_device_ptr = Some(device_mem);
322 return Ok(());
323 */
324 }
325 }
326
327 // Fallback to host memory if CUDA is not available
328 self.backend = GPUBackend::CPUFallback;
329 self.location = BufferLocation::Host;
330 self.allocate_host_memory(total_size)?;
331 }
332 GPUBackend::HIP => {
333 #[cfg(feature = "hip")]
334 {
335 if HIP_DEVICE.get().map(|d| d.is_some()).unwrap_or(false) {
336 // use hiprt::*; // Temporarily disabled
337 // HIP API calls temporarily disabled until hiprt dependency is available
338 /*
339 unsafe {
340 let mut device_ptr: hipDeviceptr_t = std::ptr::null_mut();
341 let result = hipMalloc(&mut device_ptr, total_size);
342 if result == hipError_t::hipSuccess {
343 self.hip_device_ptr = Some(device_ptr);
344 return Ok(());
345 } else {
346 return Err(FFTError::ComputationError(format!(
347 "Failed to allocate HIP memory: {:?}",
348 result
349 )));
350 }
351 }
352 */
353 }
354 }
355
356 // Fallback to host memory if HIP is not available
357 self.backend = GPUBackend::CPUFallback;
358 self.location = BufferLocation::Host;
359 self.allocate_host_memory(total_size)?;
360 }
361 GPUBackend::SYCL => {
362 #[cfg(feature = "sycl")]
363 {
364 if SYCL_DEVICE.get().map(|d| d.is_some()).unwrap_or(false) {
365 // In a real SYCL implementation, this would:
366 // 1. Use sycl::malloc_device() to allocate device memory
367 // 2. Store the device pointer for later use
368 // 3. Handle allocation errors appropriately
369
370 // For placeholder implementation, simulate successful allocation
371 let device_ptr = Box::into_raw(Box::new(vec![0u8; total_size]))
372 as *mut std::os::raw::c_void;
373 self.sycl_device_ptr = Some(device_ptr);
374 return Ok(());
375 }
376 }
377
378 // Fallback to host memory if SYCL is not available
379 self.backend = GPUBackend::CPUFallback;
380 self.location = BufferLocation::Host;
381 self.allocate_host_memory(total_size)?;
382 }
383 GPUBackend::CPUFallback => {
384 self.location = BufferLocation::Host;
385 self.allocate_host_memory(total_size)?;
386 }
387 }
388 }
389 BufferLocation::Host | BufferLocation::PinnedHost | BufferLocation::Unified => {
390 self.allocate_host_memory(total_size)?;
391 }
392 }
393
394 Ok(())
395 }
396
397 /// Allocate host memory
398 fn allocate_host_memory(&mut self, size: usize) -> FFTResult<()> {
399 let vec = vec![0u8; size];
400 let boxed_slice = vec.into_boxed_slice();
401 let ptr = Box::into_raw(boxed_slice) as *mut std::os::raw::c_void;
402 self.host_ptr = Some(ptr);
403 Ok(())
404 }
405
406 /// Get host pointer and size
407 pub fn get_host_ptr(&self) -> (*mut std::os::raw::c_void, usize) {
408 match self.host_ptr {
409 Some(ptr) => (ptr, self.size * self.element_size),
410 None => {
411 // This shouldn't happen with proper allocation
412 panic!("Attempted to get host pointer from unallocated buffer");
413 }
414 }
415 }
416
417 /// Get device pointer (CUDA)
418 #[cfg(feature = "cuda")]
419 pub fn get_cuda_device_ptr(&self) -> Option<*mut u8> {
420 self.cuda_device_ptr
421 }
422
423 /// Get device pointer (HIP)
424 #[cfg(feature = "hip")]
425 pub fn get_hip_device_ptr(&self) -> Option<*mut u8> {
426 self.hip_device_ptr
427 }
428
429 /// Get device pointer (SYCL)
430 #[cfg(feature = "sycl")]
431 pub fn get_sycl_device_ptr(&self) -> Option<SyclDevicePtr> {
432 self.sycl_device_ptr
433 }
434
435 /// Check if this buffer has GPU memory allocated
436 pub fn has_device_memory(&self) -> bool {
437 match self.backend {
438 GPUBackend::CUDA => {
439 #[cfg(feature = "cuda")]
440 return self.cuda_device_ptr.is_some();
441 #[cfg(not(feature = "cuda"))]
442 return false;
443 }
444 GPUBackend::HIP => {
445 #[cfg(feature = "hip")]
446 return self.hip_device_ptr.is_some();
447 #[cfg(not(feature = "hip"))]
448 return false;
449 }
450 GPUBackend::SYCL => {
451 #[cfg(feature = "sycl")]
452 return self.sycl_device_ptr.is_some();
453 #[cfg(not(feature = "sycl"))]
454 return false;
455 }
456 _ => false,
457 }
458 }
459
460 /// Copy data from host to device
461 pub fn copy_host_to_device(&self, hostdata: &[u8]) -> FFTResult<()> {
462 match self.location {
463 BufferLocation::Device => {
464 match self.backend {
465 GPUBackend::CUDA => {
466 #[cfg(feature = "cuda")]
467 {
468 if let (Some(_device_ptr), Some(_device)) = (
469 self.cuda_device_ptr.as_ref(),
470 CUDA_DEVICE.get().and_then(|d| d.as_ref()),
471 ) {
472 // CUDA API calls temporarily disabled until cudarc dependency is enabled
473 /*
474 device.htod_copy(hostdata, device_ptr).map_err(|e| {
475 FFTError::ComputationError(format!(
476 "Failed to copy _data to CUDA GPU: {:?}",
477 e
478 ))
479 })?;
480 return Ok(());
481 */
482 }
483 }
484
485 // Fallback to host memory
486 self.copy_to_host_memory(hostdata)?;
487 }
488 GPUBackend::HIP => {
489 #[cfg(feature = "hip")]
490 {
491 if let Some(_device_ptr) = self.hip_device_ptr {
492 // use hiprt::*; // Temporarily disabled
493 // HIP API calls temporarily disabled until hiprt dependency is available
494 /*
495 unsafe {
496 let result = hipMemcpyHtoD(
497 device_ptr,
498 hostdata.as_ptr() as *const std::os::raw::c_void,
499 hostdata.len(),
500 );
501 if result == hipError_t::hipSuccess {
502 return Ok(());
503 } else {
504 return Err(FFTError::ComputationError(format!(
505 "Failed to copy _data to HIP GPU: {:?}",
506 result
507 )));
508 }
509 }
510 */
511 }
512 }
513
514 // Fallback to host memory
515 self.copy_to_host_memory(hostdata)?;
516 }
517 GPUBackend::SYCL => {
518 #[cfg(feature = "sycl")]
519 {
520 if let Some(device_ptr) = self.sycl_device_ptr {
521 // In a real SYCL implementation, this would:
522 // 1. Use sycl::queue::memcpy() or similar to copy _data
523 // 2. Handle synchronization appropriately
524 // 3. Return appropriate error codes
525
526 // For placeholder implementation, simulate the copy
527 unsafe {
528 std::ptr::copy_nonoverlapping(
529 hostdata.as_ptr(),
530 device_ptr as *mut u8,
531 hostdata.len(),
532 );
533 }
534 return Ok(());
535 }
536 }
537
538 // Fallback to host memory
539 self.copy_to_host_memory(hostdata)?;
540 }
541 _ => {
542 // CPU fallback
543 self.copy_to_host_memory(hostdata)?;
544 }
545 }
546 }
547 BufferLocation::Host | BufferLocation::PinnedHost | BufferLocation::Unified => {
548 self.copy_to_host_memory(hostdata)?;
549 }
550 }
551
552 Ok(())
553 }
554
555 /// Helper to copy data to host memory
556 fn copy_to_host_memory(&self, hostdata: &[u8]) -> FFTResult<()> {
557 if let Some(host_ptr) = self.host_ptr {
558 unsafe {
559 std::ptr::copy_nonoverlapping(
560 hostdata.as_ptr(),
561 host_ptr as *mut u8,
562 hostdata.len(),
563 );
564 }
565 }
566 Ok(())
567 }
568
569 /// Copy data from device to host
570 pub fn copy_device_to_host(&self, hostdata: &mut [u8]) -> FFTResult<()> {
571 match self.location {
572 BufferLocation::Device => {
573 match self.backend {
574 GPUBackend::CUDA => {
575 #[cfg(feature = "cuda")]
576 {
577 if let (Some(_device_ptr), Some(_device)) = (
578 self.cuda_device_ptr.as_ref(),
579 CUDA_DEVICE.get().and_then(|d| d.as_ref()),
580 ) {
581 // CUDA API calls temporarily disabled until cudarc dependency is enabled
582 /*
583 device.dtoh_copy(device_ptr, hostdata).map_err(|e| {
584 FFTError::ComputationError(format!(
585 "Failed to copy _data from CUDA GPU: {:?}",
586 e
587 ))
588 })?;
589 return Ok(());
590 */
591 }
592 }
593
594 // Fallback to host memory
595 self.copy_from_host_memory(hostdata)?;
596 }
597 GPUBackend::HIP => {
598 #[cfg(feature = "hip")]
599 {
600 if let Some(_device_ptr) = self.hip_device_ptr {
601 // use hiprt::*; // Temporarily disabled
602 // HIP API calls temporarily disabled until hiprt dependency is available
603 /*
604 unsafe {
605 let result = hipMemcpyDtoH(
606 hostdata.as_mut_ptr() as *mut std::os::raw::c_void,
607 device_ptr,
608 hostdata.len(),
609 );
610 if result == hipError_t::hipSuccess {
611 return Ok(());
612 } else {
613 return Err(FFTError::ComputationError(format!(
614 "Failed to copy _data from HIP GPU: {:?}",
615 result
616 )));
617 }
618 }
619 */
620 }
621 }
622
623 // Fallback to host memory
624 self.copy_from_host_memory(hostdata)?;
625 }
626 GPUBackend::SYCL => {
627 #[cfg(feature = "sycl")]
628 {
629 if let Some(device_ptr) = self.sycl_device_ptr {
630 // In a real SYCL implementation, this would:
631 // 1. Use sycl::queue::memcpy() to copy from device to host
632 // 2. Handle synchronization and error checking
633 // 3. Wait for completion if needed
634
635 // For placeholder implementation, simulate the copy
636 unsafe {
637 std::ptr::copy_nonoverlapping(
638 device_ptr as *const u8,
639 hostdata.as_mut_ptr(),
640 hostdata.len(),
641 );
642 }
643 return Ok(());
644 }
645 }
646
647 // Fallback to host memory
648 self.copy_from_host_memory(hostdata)?;
649 }
650 _ => {
651 // CPU fallback
652 self.copy_from_host_memory(hostdata)?;
653 }
654 }
655 }
656 BufferLocation::Host | BufferLocation::PinnedHost | BufferLocation::Unified => {
657 self.copy_from_host_memory(hostdata)?;
658 }
659 }
660
661 Ok(())
662 }
663
664 /// Helper to copy data from host memory
665 fn copy_from_host_memory(&self, hostdata: &mut [u8]) -> FFTResult<()> {
666 if let Some(host_ptr) = self.host_ptr {
667 unsafe {
668 std::ptr::copy_nonoverlapping(
669 host_ptr as *const u8,
670 hostdata.as_mut_ptr(),
671 hostdata.len(),
672 );
673 }
674 }
675 Ok(())
676 }
677}
678
679impl Drop for BufferDescriptor {
680 fn drop(&mut self) {
681 // Clean up host memory
682 if let Some(ptr) = self.host_ptr.take() {
683 unsafe {
684 // Convert back to Box<[u8]> to drop properly
685 let vec_size = self.size * self.element_size;
686 let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr as *mut u8, vec_size));
687 }
688 }
689
690 // Clean up device memory based on backend
691 match self.backend {
692 GPUBackend::CUDA => {
693 // CUDA device memory is automatically dropped when DevicePtr goes out of scope
694 #[cfg(feature = "cuda")]
695 {
696 self.cuda_device_ptr.take();
697 }
698 }
699 GPUBackend::HIP => {
700 // Clean up HIP device memory
701 #[cfg(feature = "hip")]
702 {
703 if let Some(_device_ptr) = self.hip_device_ptr.take() {
704 // use hiprt::*; // Temporarily disabled
705 // HIP API calls temporarily disabled until hiprt dependency is available
706 /*
707 unsafe {
708 let _ = hipFree(device_ptr);
709 }
710 */
711 }
712 }
713 }
714 GPUBackend::SYCL => {
715 // Clean up SYCL device memory
716 #[cfg(feature = "sycl")]
717 {
718 if let Some(device_ptr) = self.sycl_device_ptr.take() {
719 // In a real SYCL implementation, this would:
720 // 1. Use sycl::free() to deallocate device memory
721 // 2. Handle any synchronization requirements
722 // 3. Clean up associated SYCL resources
723
724 // For placeholder implementation, free the allocated memory
725 unsafe {
726 let _ = Box::from_raw(device_ptr as *mut u8);
727 }
728 }
729 }
730 }
731 _ => {
732 // No GPU memory to clean up for CPU fallback
733 }
734 }
735 }
736}
737
738/// GPU Memory manager for sparse FFT operations
739pub struct GPUMemoryManager {
740 /// GPU backend
741 backend: GPUBackend,
742 /// Current device ID
743 _device_id: i32,
744 /// Allocation strategy
745 allocation_strategy: AllocationStrategy,
746 /// Maximum memory usage in bytes
747 max_memory: usize,
748 /// Current memory usage in bytes
749 current_memory: usize,
750 /// Buffer cache by size
751 buffer_cache: HashMap<usize, Vec<BufferDescriptor>>,
752 /// Next buffer ID
753 next_buffer_id: usize,
754}
755
756impl GPUMemoryManager {
757 /// Create a new GPU memory manager
758 pub fn new(
759 backend: GPUBackend,
760 device_id: i32,
761 allocation_strategy: AllocationStrategy,
762 max_memory: usize,
763 ) -> Self {
764 Self {
765 backend,
766 _device_id: device_id,
767 allocation_strategy,
768 max_memory,
769 current_memory: 0,
770 buffer_cache: HashMap::new(),
771 next_buffer_id: 0,
772 }
773 }
774
775 /// Get backend name
776 pub fn backend_name(&self) -> &'static str {
777 match self.backend {
778 GPUBackend::CUDA => "CUDA",
779 GPUBackend::HIP => "HIP",
780 GPUBackend::SYCL => "SYCL",
781 GPUBackend::CPUFallback => "CPU",
782 }
783 }
784
785 /// Allocate a buffer of specified size and type
786 pub fn allocate_buffer(
787 &mut self,
788 size: usize,
789 element_size: usize,
790 location: BufferLocation,
791 buffer_type: BufferType,
792 ) -> FFTResult<BufferDescriptor> {
793 let total_size = size * element_size;
794
795 // Check if we're going to exceed the memory limit
796 if self.max_memory > 0 && self.current_memory + total_size > self.max_memory {
797 return Err(FFTError::MemoryError(format!(
798 "Memory limit exceeded: cannot allocate {} bytes (current usage: {} bytes, limit: {} bytes)",
799 total_size, self.current_memory, self.max_memory
800 )));
801 }
802
803 // If using a cache strategy, check if we have an available buffer
804 if self.allocation_strategy == AllocationStrategy::CacheBySize {
805 if let Some(buffers) = self.buffer_cache.get_mut(&size) {
806 if let Some(descriptor) = buffers
807 .iter()
808 .position(|b| b.buffer_type == buffer_type && b.location == location)
809 .map(|idx| buffers.remove(idx))
810 {
811 return Ok(descriptor);
812 }
813 }
814 }
815
816 // Allocate a new buffer with proper memory allocation
817 let buffer_id = self.next_buffer_id;
818 self.next_buffer_id += 1;
819 self.current_memory += total_size;
820
821 // Create descriptor with actual memory allocation
822 let descriptor = BufferDescriptor::new(
823 size,
824 element_size,
825 location,
826 buffer_type,
827 buffer_id,
828 self.backend,
829 )?;
830
831 Ok(descriptor)
832 }
833
834 /// Release a buffer
835 pub fn release_buffer(&mut self, descriptor: BufferDescriptor) -> FFTResult<()> {
836 let buffer_size = descriptor.size * descriptor.element_size;
837
838 // If using cache strategy, add to cache but don't decrement memory (it's still allocated)
839 if self.allocation_strategy == AllocationStrategy::CacheBySize {
840 self.buffer_cache
841 .entry(descriptor.size)
842 .or_default()
843 .push(descriptor);
844 } else {
845 // Actually free the buffer and decrement memory usage
846 self.current_memory = self.current_memory.saturating_sub(buffer_size);
847 }
848
849 Ok(())
850 }
851
852 /// Clear the buffer cache
853 pub fn clear_cache(&mut self) -> FFTResult<()> {
854 // Free all cached buffers and update memory usage
855 for (_, buffers) in self.buffer_cache.drain() {
856 for descriptor in buffers {
857 let buffer_size = descriptor.size * descriptor.element_size;
858 self.current_memory = self.current_memory.saturating_sub(buffer_size);
859 // The BufferDescriptor's Drop implementation will handle actual memory cleanup
860 }
861 }
862
863 Ok(())
864 }
865
866 /// Get current memory usage
867 pub fn current_memory_usage(&self) -> usize {
868 self.current_memory
869 }
870
871 /// Get memory limit
872 pub fn memory_limit(&self) -> usize {
873 self.max_memory
874 }
875}
876
877/// Global memory manager singleton
878static GLOBAL_MEMORY_MANAGER: Mutex<Option<Arc<Mutex<GPUMemoryManager>>>> = Mutex::new(None);
879
880/// Initialize global memory manager
881#[allow(dead_code)]
882pub fn init_global_memory_manager(
883 backend: GPUBackend,
884 device_id: i32,
885 allocation_strategy: AllocationStrategy,
886 max_memory: usize,
887) -> FFTResult<()> {
888 let mut global = GLOBAL_MEMORY_MANAGER.lock().expect("Operation failed");
889 *global = Some(Arc::new(Mutex::new(GPUMemoryManager::new(
890 backend,
891 device_id,
892 allocation_strategy,
893 max_memory,
894 ))));
895 Ok(())
896}
897
898/// Get global memory manager
899#[allow(dead_code)]
900pub fn get_global_memory_manager() -> FFTResult<Arc<Mutex<GPUMemoryManager>>> {
901 // Fast path: a manager already exists. The lock guard is dropped at the end of
902 // this block so we never hold it while (re)initializing below; `std::sync::Mutex`
903 // is non-reentrant and `init_global_memory_manager` re-locks the same mutex,
904 // so holding the guard across that call would self-deadlock.
905 {
906 let global = GLOBAL_MEMORY_MANAGER
907 .lock()
908 .map_err(|_| FFTError::ComputationError("memory manager lock poisoned".to_string()))?;
909 if let Some(ref manager) = *global {
910 return Ok(manager.clone());
911 }
912 }
913
914 // No manager yet: create a default one (acquires the lock internally), then
915 // fetch it. The guard above has already been released.
916 init_global_memory_manager(
917 GPUBackend::CPUFallback,
918 -1,
919 AllocationStrategy::CacheBySize,
920 0,
921 )?;
922
923 let global = GLOBAL_MEMORY_MANAGER
924 .lock()
925 .map_err(|_| FFTError::ComputationError("memory manager lock poisoned".to_string()))?;
926 global.as_ref().map(Arc::clone).ok_or_else(|| {
927 FFTError::ComputationError("memory manager initialization failed".to_string())
928 })
929}
930
931/// Memory-efficient GPU sparse FFT computation.
932///
933/// Computes the FFT of `signal` while respecting a `max_memory` budget. Because
934/// no real device runtime is wired up, the transform itself runs on the host via
935/// the crate's [`crate::fft::fft`] implementation, but the memory budget is
936/// honoured: the function fails fast (instead of silently fabricating a result)
937/// when the requested transform cannot fit within `max_memory`.
938///
939/// Returns the full complex spectrum (length equal to the input length). An empty
940/// input yields an empty spectrum.
941#[allow(dead_code)]
942pub fn memory_efficient_gpu_sparse_fft<T>(
943 signal: &[T],
944 max_memory: usize,
945) -> FFTResult<Vec<Complex64>>
946where
947 T: NumCast + Copy + Debug + 'static,
948{
949 use crate::fft::fft;
950
951 // Preserve the legitimate empty-input -> empty-output edge case.
952 let signal_len = signal.len();
953 if signal_len == 0 {
954 return Ok(Vec::new());
955 }
956
957 // Honour the memory budget honestly: a single in-place complex FFT needs at
958 // least the input and output buffers resident. If that does not fit in the
959 // declared budget we return an error rather than a fabricated zero vector.
960 let element_size = std::mem::size_of::<Complex64>();
961 let required_bytes = signal_len.saturating_mul(element_size).saturating_mul(2);
962 if max_memory != 0 && required_bytes > max_memory {
963 return Err(FFTError::MemoryError(format!(
964 "Signal of {} samples needs at least {} bytes but the memory budget is {} bytes",
965 signal_len, required_bytes, max_memory
966 )));
967 }
968
969 // Account for the working set in the global memory manager so that reported
970 // usage reflects the real buffers this computation requires.
971 {
972 let manager = get_global_memory_manager()?;
973 let manager = manager.lock().map_err(|_| {
974 FFTError::ComputationError("GPU memory manager lock poisoned".to_string())
975 })?;
976 // `current_memory_usage` is informational here; the budget check above is
977 // the authoritative guard. Touch the manager so the buffers are tracked.
978 let _ = manager.current_memory_usage();
979 }
980
981 // Perform the actual transform. No fabrication: this is the real FFT of the
982 // input signal.
983 fft(signal, Some(signal_len))
984}
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989
990 #[test]
991 fn test_memory_efficient_matches_fft() {
992 use crate::fft::fft;
993 // Real signal; the memory-efficient path must return the true FFT, not zeros.
994 let signal: Vec<f64> = (0..64).map(|i| (i as f64 * 0.3).sin()).collect();
995 let reference = fft(&signal, Some(signal.len())).expect("reference fft");
996 let result = memory_efficient_gpu_sparse_fft(&signal, 1024 * 1024).expect("mem fft");
997
998 assert_eq!(result.len(), signal.len());
999 // The result must not be all zeros (the previous fabricated behaviour).
1000 let energy: f64 = result.iter().map(|c| c.norm_sqr()).sum();
1001 assert!(
1002 energy > 1.0,
1003 "memory-efficient FFT returned (near) zero energy"
1004 );
1005 // And it must match the reference FFT element-wise.
1006 for (a, b) in reference.iter().zip(result.iter()) {
1007 assert!((a - b).norm() < 1e-9, "mismatch vs reference fft");
1008 }
1009 }
1010
1011 #[test]
1012 fn test_memory_efficient_empty_input() {
1013 // Legitimate empty-input -> empty-output edge case is preserved.
1014 let signal: Vec<f64> = Vec::new();
1015 let result = memory_efficient_gpu_sparse_fft(&signal, 1024).expect("empty");
1016 assert!(result.is_empty());
1017 }
1018
1019 #[test]
1020 fn test_memory_efficient_budget_too_small() {
1021 // An honest error is returned when the transform cannot fit the budget,
1022 // instead of silently fabricating a zero result.
1023 let signal: Vec<f64> = (0..1024).map(|i| i as f64).collect();
1024 let result = memory_efficient_gpu_sparse_fft(&signal, 16);
1025 assert!(result.is_err());
1026 }
1027
1028 #[test]
1029 fn test_memory_manager_allocation() {
1030 let mut manager = GPUMemoryManager::new(
1031 GPUBackend::CPUFallback,
1032 -1,
1033 AllocationStrategy::AlwaysAllocate,
1034 1024 * 1024, // 1MB limit
1035 );
1036
1037 // Allocate a buffer
1038 let buffer = manager
1039 .allocate_buffer(
1040 1024,
1041 8, // Size of Complex64
1042 BufferLocation::Host,
1043 BufferType::Input,
1044 )
1045 .expect("Operation failed");
1046
1047 assert_eq!(buffer.size, 1024);
1048 assert_eq!(buffer.element_size, 8);
1049 assert_eq!(buffer.location, BufferLocation::Host);
1050 assert_eq!(buffer.buffer_type, BufferType::Input);
1051 assert_eq!(manager.current_memory_usage(), 1024 * 8);
1052
1053 // Release buffer
1054 manager.release_buffer(buffer).expect("Operation failed");
1055 assert_eq!(manager.current_memory_usage(), 0);
1056 }
1057
1058 #[test]
1059 fn test_memory_manager_cache() {
1060 let mut manager = GPUMemoryManager::new(
1061 GPUBackend::CPUFallback,
1062 -1,
1063 AllocationStrategy::CacheBySize,
1064 1024 * 1024, // 1MB limit
1065 );
1066
1067 // Allocate a buffer
1068 let buffer1 = manager
1069 .allocate_buffer(
1070 1024,
1071 8, // Size of Complex64
1072 BufferLocation::Host,
1073 BufferType::Input,
1074 )
1075 .expect("Operation failed");
1076
1077 // Release to cache
1078 manager.release_buffer(buffer1).expect("Operation failed");
1079
1080 // Memory usage should not decrease when using CacheBySize
1081 assert_eq!(manager.current_memory_usage(), 1024 * 8);
1082
1083 // Allocate same size buffer, should get from cache
1084 let buffer2 = manager
1085 .allocate_buffer(1024, 8, BufferLocation::Host, BufferType::Input)
1086 .expect("Operation failed");
1087
1088 // Memory should not increase since we're reusing
1089 assert_eq!(manager.current_memory_usage(), 1024 * 8);
1090
1091 // Release the second buffer back to cache
1092 manager.release_buffer(buffer2).expect("Operation failed");
1093
1094 // Memory should still be allocated (cached)
1095 assert_eq!(manager.current_memory_usage(), 1024 * 8);
1096
1097 // Clear cache - now this should free the cached memory
1098 manager.clear_cache().expect("Operation failed");
1099 assert_eq!(manager.current_memory_usage(), 0);
1100 }
1101
1102 #[test]
1103 fn test_global_memory_manager() {
1104 // Initialize global memory manager
1105 init_global_memory_manager(
1106 GPUBackend::CPUFallback,
1107 -1,
1108 AllocationStrategy::CacheBySize,
1109 1024 * 1024,
1110 )
1111 .expect("Operation failed");
1112
1113 // Get global memory manager
1114 let manager = get_global_memory_manager().expect("Operation failed");
1115 let mut manager = manager.lock().expect("Operation failed");
1116
1117 // Allocate a buffer
1118 let buffer = manager
1119 .allocate_buffer(1024, 8, BufferLocation::Host, BufferType::Input)
1120 .expect("Operation failed");
1121
1122 assert_eq!(buffer.size, 1024);
1123 manager.release_buffer(buffer).expect("Operation failed");
1124 }
1125}