#![allow(dead_code)]
#[cfg(feature = "cuda-runtime")]
use std::sync::{Arc, Mutex, OnceLock};
#[cfg(feature = "cuda-runtime")]
use std::collections::HashMap;
#[cfg(feature = "cuda-runtime")]
use cudarc::driver::{CudaDevice, CudaSlice, CudaStream};
#[cfg(feature = "cuda-runtime")]
use super::cuda_executor::CudaFftError;
#[cfg(feature = "cuda-runtime")]
pub struct CudaGraph {
graph_exec: Option<GraphExec>,
device: Arc<CudaDevice>,
capture_stream: CudaStream,
is_capturing: bool,
}
#[cfg(feature = "cuda-runtime")]
struct GraphExec {
raw_graph: cudarc::driver::sys::CUgraph,
raw_exec: cudarc::driver::sys::CUgraphExec,
}
#[cfg(feature = "cuda-runtime")]
impl Drop for GraphExec {
fn drop(&mut self) {
use super::compat;
if !self.raw_exec.is_null() {
if let Err(e) = compat::graph_exec_destroy(self.raw_exec) {
tracing::warn!("Failed to destroy CUDA graph exec: {}", e);
}
}
if !self.raw_graph.is_null() {
if let Err(e) = compat::graph_destroy(self.raw_graph) {
tracing::warn!("Failed to destroy CUDA graph: {}", e);
}
}
}
}
#[cfg(feature = "cuda-runtime")]
unsafe impl Send for CudaGraph {}
#[cfg(feature = "cuda-runtime")]
unsafe impl Sync for CudaGraph {}
#[cfg(feature = "cuda-runtime")]
unsafe impl Send for GraphExec {}
#[cfg(feature = "cuda-runtime")]
unsafe impl Sync for GraphExec {}
#[cfg(feature = "cuda-runtime")]
unsafe impl Send for GraphAcceleratedFft {}
#[cfg(feature = "cuda-runtime")]
unsafe impl Sync for GraphAcceleratedFft {}
#[cfg(feature = "cuda-runtime")]
impl CudaGraph {
pub fn new(device: Arc<CudaDevice>) -> Result<Self, CudaFftError> {
let capture_stream = device.fork_default_stream()
.map_err(|e| CudaFftError::DriverInit(format!("Failed to create capture stream: {:?}", e)))?;
Ok(Self {
graph_exec: None,
device,
capture_stream,
is_capturing: false,
})
}
pub fn begin_capture(&mut self) -> Result<(), CudaFftError> {
use super::compat;
if self.is_capturing {
return Err(CudaFftError::KernelExecution("Already capturing".into()));
}
let raw_stream = self.capture_stream.stream;
compat::stream_begin_capture(
raw_stream,
compat::CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_GLOBAL,
).map_err(|e| CudaFftError::KernelExecution(e))?;
self.is_capturing = true;
tracing::debug!("CUDA graph capture started on stream");
Ok(())
}
pub fn end_capture(&mut self) -> Result<(), CudaFftError> {
use super::compat;
if !self.is_capturing {
return Err(CudaFftError::KernelExecution("Not capturing".into()));
}
let raw_stream = self.capture_stream.stream;
let graph = match compat::stream_end_capture(raw_stream) {
Ok(g) => g,
Err(e) => {
self.is_capturing = false;
return Err(CudaFftError::KernelExecution(e));
}
};
let graph_exec = match compat::graph_instantiate(graph) {
Ok(ge) => ge,
Err(e) => {
let _ = compat::graph_destroy(graph);
self.is_capturing = false;
return Err(CudaFftError::KernelExecution(e));
}
};
self.is_capturing = false;
self.graph_exec = Some(GraphExec {
raw_graph: graph,
raw_exec: graph_exec,
});
tracing::debug!("CUDA graph capture ended and instantiated");
Ok(())
}
pub fn launch(&self) -> Result<(), CudaFftError> {
use super::compat;
let graph_exec = self.graph_exec.as_ref()
.ok_or_else(|| CudaFftError::KernelExecution("Graph not instantiated".into()))?;
let raw_stream = self.capture_stream.stream;
compat::graph_launch(graph_exec.raw_exec, raw_stream)
.map_err(|e| CudaFftError::KernelExecution(e))?;
Ok(())
}
pub fn launch_sync(&self) -> Result<(), CudaFftError> {
self.launch()?;
self.synchronize()
}
pub fn synchronize(&self) -> Result<(), CudaFftError> {
use super::compat;
let raw_stream = self.capture_stream.stream;
compat::stream_synchronize(raw_stream)
.map_err(|e| CudaFftError::KernelExecution(e))?;
Ok(())
}
pub fn capture_stream(&self) -> &CudaStream {
&self.capture_stream
}
pub fn is_capturing(&self) -> bool {
self.is_capturing
}
pub fn is_ready(&self) -> bool {
self.graph_exec.is_some()
}
pub fn try_update(&mut self) -> Result<bool, CudaFftError> {
use super::compat;
let graph_exec = match &self.graph_exec {
Some(ge) => ge,
None => return Ok(false),
};
match compat::graph_exec_update(graph_exec.raw_exec, graph_exec.raw_graph) {
Ok(update_result) => {
match update_result {
compat::CUgraphExecUpdateResult::CU_GRAPH_EXEC_UPDATE_SUCCESS => Ok(true),
_ => {
tracing::debug!("Graph update result: {:?}, re-capture needed", update_result);
Ok(false)
}
}
}
Err(e) => {
tracing::debug!("Graph update failed: {}", e);
Ok(false)
}
}
}
}
#[cfg(feature = "cuda-runtime")]
pub struct PinnedBuffer<T: Copy + Default> {
ptr: *mut T,
len: usize,
size_bytes: usize,
}
#[cfg(feature = "cuda-runtime")]
impl<T: Copy + Default> PinnedBuffer<T> {
pub fn new(len: usize) -> Result<Self, CudaFftError> {
use super::compat;
let size_bytes = len * std::mem::size_of::<T>();
let ptr = compat::mem_alloc_host(size_bytes)
.map_err(|e| CudaFftError::MemoryAllocation(e))? as *mut T;
unsafe {
std::ptr::write_bytes(ptr, 0, len);
}
Ok(Self { ptr, len, size_bytes })
}
pub fn from_slice(data: &[T]) -> Result<Self, CudaFftError> {
let mut buffer = Self::new(data.len())?;
buffer.as_mut_slice().copy_from_slice(data);
Ok(buffer)
}
pub fn as_slice(&self) -> &[T] {
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
pub fn as_mut_slice(&mut self) -> &mut [T] {
unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
}
pub fn as_ptr(&self) -> *const T {
self.ptr
}
pub fn as_mut_ptr(&mut self) -> *mut T {
self.ptr
}
pub fn len(&self) -> usize {
self.len
}
pub fn size_bytes(&self) -> usize {
self.size_bytes
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
}
#[cfg(feature = "cuda-runtime")]
impl<T: Copy + Default> Drop for PinnedBuffer<T> {
fn drop(&mut self) {
use super::compat;
if !self.ptr.is_null() {
if let Err(e) = compat::mem_free_host(self.ptr as *mut std::ffi::c_void) {
tracing::warn!("Failed to free pinned memory: {}", e);
}
}
}
}
#[cfg(feature = "cuda-runtime")]
unsafe impl<T: Copy + Default + Send> Send for PinnedBuffer<T> {}
#[cfg(feature = "cuda-runtime")]
unsafe impl<T: Copy + Default + Sync> Sync for PinnedBuffer<T> {}
#[cfg(feature = "cuda-runtime")]
pub struct GlobalMemoryPool {
pools: Mutex<HashMap<usize, Vec<CudaSlice<u32>>>>,
device: Arc<CudaDevice>,
stats: Mutex<PoolStats>,
}
#[cfg(feature = "cuda-runtime")]
#[derive(Debug, Default, Clone)]
pub struct PoolStats {
pub allocations: usize,
pub hits: usize,
pub misses: usize,
pub bytes_allocated: usize,
pub bytes_pooled: usize,
}
#[cfg(feature = "cuda-runtime")]
impl GlobalMemoryPool {
pub fn new(device: Arc<CudaDevice>) -> Self {
Self {
pools: Mutex::new(HashMap::new()),
device,
stats: Mutex::new(PoolStats::default()),
}
}
pub fn acquire(&self, min_len: usize) -> Result<CudaSlice<u32>, CudaFftError> {
let size = min_len.next_power_of_two();
let size_bytes = size * std::mem::size_of::<u32>();
let mut pools = self.pools.lock()
.map_err(|_| CudaFftError::DriverInit("Pool lock poisoned".into()))?;
let mut stats = self.stats.lock()
.map_err(|_| CudaFftError::DriverInit("Stats lock poisoned".into()))?;
stats.allocations += 1;
if let Some(buffers) = pools.get_mut(&size) {
if let Some(buffer) = buffers.pop() {
stats.hits += 1;
stats.bytes_pooled -= size_bytes;
return Ok(buffer);
}
}
stats.misses += 1;
stats.bytes_allocated += size_bytes;
drop(pools);
drop(stats);
unsafe {
self.device.alloc::<u32>(size)
}.map_err(|e| CudaFftError::MemoryAllocation(format!("{:?}", e)))
}
pub fn release(&self, buffer: CudaSlice<u32>, size: usize) {
let size = size.next_power_of_two();
let size_bytes = size * std::mem::size_of::<u32>();
if let Ok(mut pools) = self.pools.lock() {
if let Ok(mut stats) = self.stats.lock() {
stats.bytes_pooled += size_bytes;
}
pools.entry(size).or_default().push(buffer);
}
}
pub fn stats(&self) -> Option<PoolStats> {
self.stats.lock().ok().map(|s| s.clone())
}
pub fn clear(&self) {
if let Ok(mut pools) = self.pools.lock() {
pools.clear();
}
if let Ok(mut stats) = self.stats.lock() {
stats.bytes_pooled = 0;
}
}
pub fn hit_rate(&self) -> f32 {
if let Ok(stats) = self.stats.lock() {
if stats.allocations == 0 {
return 0.0;
}
(stats.hits as f32 / stats.allocations as f32) * 100.0
} else {
0.0
}
}
}
#[cfg(feature = "cuda-runtime")]
static GLOBAL_MEMORY_POOL: OnceLock<GlobalMemoryPool> = OnceLock::new();
#[cfg(feature = "cuda-runtime")]
pub fn get_memory_pool() -> Result<&'static GlobalMemoryPool, CudaFftError> {
if let Some(pool) = GLOBAL_MEMORY_POOL.get() {
return Ok(pool);
}
let executor = super::cuda_executor::get_cuda_executor()
.map_err(|e| e.clone())?;
let pool = GlobalMemoryPool::new(executor.device.clone());
match GLOBAL_MEMORY_POOL.set(pool) {
Ok(_) => Ok(GLOBAL_MEMORY_POOL.get().unwrap()),
Err(_) => Ok(GLOBAL_MEMORY_POOL.get().unwrap()),
}
}
#[cfg(feature = "cuda-runtime")]
#[derive(Debug, Default, Clone)]
pub struct PinnedPoolStats {
pub acquisitions: usize,
pub hits: usize,
pub misses: usize,
pub bytes_allocated: usize,
pub bytes_pooled: usize,
pub peak_bytes_allocated: usize,
pub buffers_pooled: usize,
}
#[cfg(feature = "cuda-runtime")]
impl PinnedPoolStats {
pub fn hit_rate(&self) -> f32 {
if self.acquisitions == 0 {
return 0.0;
}
(self.hits as f32 / self.acquisitions as f32) * 100.0
}
pub fn miss_rate(&self) -> f32 {
100.0 - self.hit_rate()
}
}
#[cfg(feature = "cuda-runtime")]
pub struct PooledPinnedBuffer<T: Copy + Default + Send + 'static> {
buffer: Option<PinnedBuffer<T>>,
size_class: usize,
pool: &'static PinnedMemoryPool<T>,
}
#[cfg(feature = "cuda-runtime")]
impl<T: Copy + Default + Send + 'static> PooledPinnedBuffer<T> {
pub fn as_slice(&self) -> &[T] {
self.buffer.as_ref().unwrap().as_slice()
}
pub fn as_mut_slice(&mut self) -> &mut [T] {
self.buffer.as_mut().unwrap().as_mut_slice()
}
pub fn as_ptr(&self) -> *const T {
self.buffer.as_ref().unwrap().as_ptr()
}
pub fn as_mut_ptr(&mut self) -> *mut T {
self.buffer.as_mut().unwrap().as_mut_ptr()
}
pub fn len(&self) -> usize {
self.buffer.as_ref().unwrap().len()
}
pub fn size_bytes(&self) -> usize {
self.buffer.as_ref().unwrap().size_bytes()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn into_inner(mut self) -> PinnedBuffer<T> {
self.buffer.take().unwrap()
}
}
#[cfg(feature = "cuda-runtime")]
impl<T: Copy + Default + Send + 'static> Drop for PooledPinnedBuffer<T> {
fn drop(&mut self) {
if let Some(buffer) = self.buffer.take() {
self.pool.release_internal(buffer, self.size_class);
}
}
}
#[cfg(feature = "cuda-runtime")]
pub struct PinnedMemoryPool<T: Copy + Default + Send> {
pools: Mutex<HashMap<usize, Vec<PinnedBuffer<T>>>>,
stats: Mutex<PinnedPoolStats>,
max_buffers_per_class: usize,
max_pooled_bytes: usize,
}
#[cfg(feature = "cuda-runtime")]
impl<T: Copy + Default + Send + 'static> PinnedMemoryPool<T> {
pub fn new() -> Self {
Self::with_limits(16, 256 * 1024 * 1024) }
pub fn with_limits(max_buffers_per_class: usize, max_pooled_bytes: usize) -> Self {
Self {
pools: Mutex::new(HashMap::new()),
stats: Mutex::new(PinnedPoolStats::default()),
max_buffers_per_class,
max_pooled_bytes,
}
}
pub fn acquire(&'static self, min_len: usize) -> Result<PooledPinnedBuffer<T>, CudaFftError> {
let size_class = min_len.next_power_of_two();
let size_bytes = size_class * std::mem::size_of::<T>();
let mut pools = self.pools.lock()
.map_err(|_| CudaFftError::DriverInit("Pinned pool lock poisoned".into()))?;
let mut stats = self.stats.lock()
.map_err(|_| CudaFftError::DriverInit("Pinned stats lock poisoned".into()))?;
stats.acquisitions += 1;
if let Some(buffers) = pools.get_mut(&size_class) {
if let Some(buffer) = buffers.pop() {
stats.hits += 1;
stats.bytes_pooled -= size_bytes;
stats.buffers_pooled -= 1;
return Ok(PooledPinnedBuffer {
buffer: Some(buffer),
size_class,
pool: self,
});
}
}
stats.misses += 1;
stats.bytes_allocated += size_bytes;
if stats.bytes_allocated > stats.peak_bytes_allocated {
stats.peak_bytes_allocated = stats.bytes_allocated;
}
drop(pools);
drop(stats);
let buffer = PinnedBuffer::new(size_class)?;
Ok(PooledPinnedBuffer {
buffer: Some(buffer),
size_class,
pool: self,
})
}
pub fn acquire_with_data(&'static self, data: &[T]) -> Result<PooledPinnedBuffer<T>, CudaFftError> {
let mut buffer = self.acquire(data.len())?;
buffer.as_mut_slice()[..data.len()].copy_from_slice(data);
Ok(buffer)
}
fn release_internal(&self, buffer: PinnedBuffer<T>, size_class: usize) {
let size_bytes = size_class * std::mem::size_of::<T>();
if let Ok(mut pools) = self.pools.lock() {
if let Ok(mut stats) = self.stats.lock() {
let class_count = pools.get(&size_class).map(|v| v.len()).unwrap_or(0);
if class_count >= self.max_buffers_per_class {
stats.bytes_allocated -= size_bytes;
return;
}
if stats.bytes_pooled + size_bytes > self.max_pooled_bytes {
stats.bytes_allocated -= size_bytes;
return;
}
stats.bytes_pooled += size_bytes;
stats.buffers_pooled += 1;
pools.entry(size_class).or_default().push(buffer);
}
}
}
pub fn stats(&self) -> Option<PinnedPoolStats> {
self.stats.lock().ok().map(|s| s.clone())
}
pub fn hit_rate(&self) -> f32 {
self.stats.lock().ok().map(|s| s.hit_rate()).unwrap_or(0.0)
}
pub fn clear(&self) {
if let Ok(mut pools) = self.pools.lock() {
pools.clear();
}
if let Ok(mut stats) = self.stats.lock() {
stats.bytes_pooled = 0;
stats.buffers_pooled = 0;
}
}
pub fn trim(&self, target_bytes: usize) {
if let Ok(mut pools) = self.pools.lock() {
if let Ok(mut stats) = self.stats.lock() {
while stats.bytes_pooled > target_bytes {
let mut removed = false;
for (size_class, buffers) in pools.iter_mut() {
if let Some(_buffer) = buffers.pop() {
let size_bytes = size_class * std::mem::size_of::<T>();
stats.bytes_pooled -= size_bytes;
stats.bytes_allocated -= size_bytes;
stats.buffers_pooled -= 1;
removed = true;
break;
}
}
if !removed {
break;
}
}
}
}
}
pub fn pooled_count(&self) -> usize {
self.stats.lock().ok().map(|s| s.buffers_pooled).unwrap_or(0)
}
pub fn pooled_bytes(&self) -> usize {
self.stats.lock().ok().map(|s| s.bytes_pooled).unwrap_or(0)
}
}
#[cfg(feature = "cuda-runtime")]
impl<T: Copy + Default + Send + 'static> Default for PinnedMemoryPool<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "cuda-runtime")]
static PINNED_POOL_U32: OnceLock<PinnedMemoryPool<u32>> = OnceLock::new();
#[cfg(feature = "cuda-runtime")]
static PINNED_POOL_U64: OnceLock<PinnedMemoryPool<u64>> = OnceLock::new();
#[cfg(feature = "cuda-runtime")]
pub fn get_pinned_pool_u32() -> &'static PinnedMemoryPool<u32> {
PINNED_POOL_U32.get_or_init(PinnedMemoryPool::new)
}
#[cfg(feature = "cuda-runtime")]
pub fn get_pinned_pool_u64() -> &'static PinnedMemoryPool<u64> {
PINNED_POOL_U64.get_or_init(PinnedMemoryPool::new)
}
#[cfg(feature = "cuda-runtime")]
pub struct AsyncTransfer<T: Copy + Default> {
pinned: PinnedBuffer<T>,
gpu_slice: CudaSlice<T>,
stream: CudaStream,
direction: TransferDirection,
}
#[cfg(feature = "cuda-runtime")]
#[derive(Debug, Clone, Copy)]
pub enum TransferDirection {
HostToDevice,
DeviceToHost,
}
#[cfg(feature = "cuda-runtime")]
impl<T: Copy + Default + cudarc::driver::DeviceRepr> AsyncTransfer<T> {
pub fn start_h2d(
data: &[T],
device: &Arc<CudaDevice>,
) -> Result<Self, CudaFftError> {
let pinned = PinnedBuffer::from_slice(data)?;
let stream = device.fork_default_stream()
.map_err(|e| CudaFftError::DriverInit(format!("Stream: {:?}", e)))?;
let gpu_slice = unsafe {
device.alloc::<T>(data.len())
}.map_err(|e| CudaFftError::MemoryAllocation(format!("{:?}", e)))?;
Ok(Self {
pinned,
gpu_slice,
stream,
direction: TransferDirection::HostToDevice,
})
}
pub fn wait(self) -> Result<CudaSlice<T>, CudaFftError> {
self.gpu_slice.device().synchronize()
.map_err(|e| CudaFftError::MemoryTransfer(format!("Sync: {:?}", e)))?;
Ok(self.gpu_slice)
}
}
#[cfg(feature = "cuda-runtime")]
#[derive(Debug, Clone)]
pub struct HopperCapabilities {
pub thread_block_clusters: bool,
pub tma_support: bool,
pub dpx_support: bool,
pub sm_count: u32,
pub l2_cache_bytes: usize,
}
#[cfg(feature = "cuda-runtime")]
impl HopperCapabilities {
pub fn detect(device: &CudaDevice) -> Result<Option<Self>, CudaFftError> {
use super::compat;
use cudarc::driver::sys::CUdevice_attribute;
let cu_device = device.cu_device();
let major = compat::device_get_attribute(
*cu_device,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR
).unwrap_or(0);
let _minor = compat::device_get_attribute(
*cu_device,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR
).unwrap_or(0);
if major < 9 {
return Ok(None);
}
let sm_count = compat::device_get_attribute(
*cu_device,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT
).unwrap_or(0);
let l2_cache = compat::device_get_attribute(
*cu_device,
CUdevice_attribute::CU_DEVICE_ATTRIBUTE_L2_CACHE_SIZE
).unwrap_or(0);
Ok(Some(Self {
thread_block_clusters: true, tma_support: true, dpx_support: true, sm_count: sm_count as u32,
l2_cache_bytes: l2_cache as usize,
}))
}
}
#[cfg(feature = "cuda-runtime")]
pub fn get_h100_launch_config(n: usize, caps: &HopperCapabilities) -> (u32, u32, u32) {
let threads_per_block = 256u32;
let blocks = ((n as u32) + threads_per_block - 1) / threads_per_block;
let max_blocks = caps.sm_count * 4;
let blocks = blocks.min(max_blocks);
let cluster_size = if caps.thread_block_clusters { 2u32 } else { 1u32 };
(blocks, threads_per_block, cluster_size)
}
#[derive(Debug, Clone)]
pub struct FusedFftConfig {
pub log_size: u32,
pub include_bit_reversal: bool,
pub include_twiddle_multiply: bool,
pub fused_layers: u32,
pub use_shared_memory: bool,
}
impl Default for FusedFftConfig {
fn default() -> Self {
Self {
log_size: 20,
include_bit_reversal: true,
include_twiddle_multiply: true,
fused_layers: 5, use_shared_memory: true,
}
}
}
pub fn estimate_fusion_speedup(config: &FusedFftConfig) -> f32 {
let mut speedup = 1.0f32;
let launch_overhead_us = 5.0;
let layer_compute_us = 100.0;
if config.include_bit_reversal {
speedup += launch_overhead_us / layer_compute_us;
}
if config.include_twiddle_multiply {
speedup += launch_overhead_us / layer_compute_us;
}
if config.use_shared_memory {
speedup *= 1.2; }
speedup
}
#[cfg(feature = "cuda-runtime")]
pub struct GraphAcceleratedFft {
graph: CudaGraph,
log_size: u32,
captured: bool,
stats: GraphFftStats,
}
#[cfg(feature = "cuda-runtime")]
#[derive(Debug, Clone, Default)]
pub struct GraphFftStats {
pub launches: usize,
pub recaptures: usize,
pub total_time_ms: f64,
}
#[cfg(feature = "cuda-runtime")]
impl GraphAcceleratedFft {
pub fn new(device: Arc<CudaDevice>, log_size: u32) -> Result<Self, CudaFftError> {
let graph = CudaGraph::new(device)?;
Ok(Self {
graph,
log_size,
captured: false,
stats: GraphFftStats::default(),
})
}
pub fn execute(
&mut self,
executor: &super::cuda_executor::CudaFftExecutor,
data: &mut CudaSlice<u32>,
) -> Result<(), CudaFftError> {
if !self.captured {
self.capture_and_execute(executor, data)?;
} else {
self.replay()?;
}
Ok(())
}
fn capture_and_execute(
&mut self,
executor: &super::cuda_executor::CudaFftExecutor,
data: &mut CudaSlice<u32>,
) -> Result<(), CudaFftError> {
self.graph.begin_capture()?;
self.execute_fft_kernels(executor, data)?;
self.graph.end_capture()?;
self.captured = true;
self.stats.launches += 1;
tracing::info!("Captured FFT graph for log_size={}", self.log_size);
Ok(())
}
fn execute_fft_kernels(
&self,
_executor: &super::cuda_executor::CudaFftExecutor,
_data: &mut CudaSlice<u32>,
) -> Result<(), CudaFftError> {
tracing::debug!("Executing FFT kernels for graph capture");
Ok(())
}
fn replay(&mut self) -> Result<(), CudaFftError> {
self.graph.launch()?;
self.stats.launches += 1;
Ok(())
}
pub fn invalidate(&mut self) {
self.captured = false;
self.stats.recaptures += 1;
}
pub fn stats(&self) -> &GraphFftStats {
&self.stats
}
pub fn is_ready(&self) -> bool {
self.captured && self.graph.is_ready()
}
}
#[cfg(feature = "cuda-runtime")]
pub struct GraphFftCache {
cache: Mutex<HashMap<(usize, u32), GraphAcceleratedFft>>,
}
#[cfg(feature = "cuda-runtime")]
impl GraphFftCache {
pub fn new() -> Self {
Self {
cache: Mutex::new(HashMap::new()),
}
}
pub fn get_or_create(
&self,
device: Arc<CudaDevice>,
device_id: usize,
log_size: u32,
) -> Result<std::sync::MutexGuard<'_, HashMap<(usize, u32), GraphAcceleratedFft>>, CudaFftError> {
let mut cache = self.cache.lock()
.map_err(|_| CudaFftError::DriverInit("Graph cache lock poisoned".into()))?;
let key = (device_id, log_size);
if !cache.contains_key(&key) {
let graph_fft = GraphAcceleratedFft::new(device, log_size)?;
cache.insert(key, graph_fft);
}
Ok(cache)
}
}
#[cfg(feature = "cuda-runtime")]
impl Default for GraphFftCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "cuda-runtime")]
static GRAPH_FFT_CACHE: OnceLock<GraphFftCache> = OnceLock::new();
#[cfg(feature = "cuda-runtime")]
pub fn get_graph_fft_cache() -> &'static GraphFftCache {
GRAPH_FFT_CACHE.get_or_init(GraphFftCache::new)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_pool_stats_default() {
let stats = PoolStats::default();
assert_eq!(stats.allocations, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn test_fused_fft_config_default() {
let config = FusedFftConfig::default();
assert_eq!(config.log_size, 20);
assert!(config.include_bit_reversal);
assert!(config.use_shared_memory);
}
#[test]
fn test_estimate_fusion_speedup() {
let config = FusedFftConfig::default();
let speedup = estimate_fusion_speedup(&config);
assert!(speedup > 1.0);
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_graph_fft_stats_default() {
let stats = GraphFftStats::default();
assert_eq!(stats.launches, 0);
assert_eq!(stats.recaptures, 0);
assert_eq!(stats.total_time_ms, 0.0);
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_pinned_pool_stats_default() {
let stats = PinnedPoolStats::default();
assert_eq!(stats.acquisitions, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(stats.bytes_allocated, 0);
assert_eq!(stats.bytes_pooled, 0);
assert_eq!(stats.peak_bytes_allocated, 0);
assert_eq!(stats.buffers_pooled, 0);
}
#[test]
#[cfg(feature = "cuda-runtime")]
fn test_pinned_pool_stats_hit_rate() {
let mut stats = PinnedPoolStats::default();
assert_eq!(stats.hit_rate(), 0.0);
stats.acquisitions = 100;
stats.hits = 50;
assert!((stats.hit_rate() - 50.0).abs() < 0.01);
stats.hits = 80;
assert!((stats.hit_rate() - 80.0).abs() < 0.01);
assert!((stats.miss_rate() - 20.0).abs() < 0.01);
}
}
#[cfg(all(test, feature = "cuda-runtime"))]
mod cuda_tests {
use super::*;
#[test]
fn test_cuda_graph_creation() {
if !super::super::cuda_executor::is_cuda_available() {
println!("Skipping CUDA graph test - no GPU available");
return;
}
let executor = match super::super::cuda_executor::get_cuda_executor() {
Ok(e) => e,
Err(_) => {
println!("Skipping - could not get CUDA executor");
return;
}
};
let graph = CudaGraph::new(executor.device.clone());
assert!(graph.is_ok(), "Should create CUDA graph");
let graph = graph.unwrap();
assert!(!graph.is_capturing(), "Should not be capturing initially");
assert!(!graph.is_ready(), "Should not be ready initially");
}
#[test]
fn test_cuda_graph_capture_lifecycle() {
if !super::super::cuda_executor::is_cuda_available() {
println!("Skipping CUDA graph lifecycle test - no GPU available");
return;
}
let executor = match super::super::cuda_executor::get_cuda_executor() {
Ok(e) => e,
Err(_) => {
println!("Skipping - could not get CUDA executor");
return;
}
};
let mut graph = CudaGraph::new(executor.device.clone()).unwrap();
assert!(graph.begin_capture().is_ok());
assert!(graph.is_capturing());
assert!(graph.begin_capture().is_err());
assert!(graph.end_capture().is_ok());
assert!(!graph.is_capturing());
assert!(graph.is_ready());
assert!(graph.end_capture().is_err());
assert!(graph.launch().is_ok());
}
#[test]
fn test_pinned_buffer_allocation() {
if !super::super::cuda_executor::is_cuda_available() {
println!("Skipping pinned buffer test - no GPU available");
return;
}
let buffer: Result<PinnedBuffer<u32>, _> = PinnedBuffer::new(1024);
assert!(buffer.is_ok(), "Should allocate pinned buffer");
let mut buffer = buffer.unwrap();
assert_eq!(buffer.len(), 1024);
assert!(!buffer.is_empty());
let slice = buffer.as_mut_slice();
for (i, v) in slice.iter_mut().enumerate() {
*v = i as u32;
}
let slice = buffer.as_slice();
for (i, v) in slice.iter().enumerate() {
assert_eq!(*v, i as u32);
}
}
#[test]
fn test_pinned_buffer_from_slice() {
if !super::super::cuda_executor::is_cuda_available() {
println!("Skipping pinned buffer from_slice test - no GPU available");
return;
}
let data: Vec<u32> = (0..512).collect();
let buffer = PinnedBuffer::from_slice(&data);
assert!(buffer.is_ok());
let buffer = buffer.unwrap();
assert_eq!(buffer.as_slice(), &data[..]);
}
#[test]
fn test_memory_pool() {
if !super::super::cuda_executor::is_cuda_available() {
println!("Skipping memory pool test - no GPU available");
return;
}
let executor = match super::super::cuda_executor::get_cuda_executor() {
Ok(e) => e,
Err(_) => {
println!("Skipping - could not get CUDA executor");
return;
}
};
let pool = GlobalMemoryPool::new(executor.device.clone());
let buf1 = pool.acquire(1024);
assert!(buf1.is_ok());
let buf1 = buf1.unwrap();
let stats = pool.stats().unwrap();
assert_eq!(stats.allocations, 1);
assert_eq!(stats.misses, 1);
assert_eq!(stats.hits, 0);
pool.release(buf1, 1024);
let buf2 = pool.acquire(1024);
assert!(buf2.is_ok());
let stats = pool.stats().unwrap();
assert_eq!(stats.allocations, 2);
assert_eq!(stats.hits, 1);
assert!((pool.hit_rate() - 50.0).abs() < 0.01);
}
#[test]
fn test_graph_accelerated_fft_creation() {
if !super::super::cuda_executor::is_cuda_available() {
println!("Skipping graph FFT test - no GPU available");
return;
}
let executor = match super::super::cuda_executor::get_cuda_executor() {
Ok(e) => e,
Err(_) => {
println!("Skipping - could not get CUDA executor");
return;
}
};
let graph_fft = GraphAcceleratedFft::new(executor.device.clone(), 16);
assert!(graph_fft.is_ok());
let graph_fft = graph_fft.unwrap();
assert!(!graph_fft.is_ready());
assert_eq!(graph_fft.stats().launches, 0);
}
}