Skip to main content

scirs2_fft/
sparse_fft_gpu_kernels.rs

1//! GPU kernel implementations for sparse FFT algorithms
2//!
3//! This module contains kernel implementations for various sparse FFT algorithms
4//! targeted at GPU acceleration. These kernels are designed to be highly
5//! optimized for specific GPU architectures and can be used with different
6//! GPU backends (CUDA, HIP, SYCL).
7
8use crate::error::{FFTError, FFTResult};
9use crate::sparse_fft::{
10    SparseFFT, SparseFFTAlgorithm, SparseFFTConfig, SparsityEstimationMethod, WindowFunction,
11};
12use scirs2_core::numeric::Complex64;
13use scirs2_core::numeric::NumCast;
14use scirs2_core::simd_ops::PlatformCapabilities;
15use std::fmt::Debug;
16
17/// GPU kernel configuration
18#[derive(Debug, Clone)]
19pub struct KernelConfig {
20    /// Block size for GPU kernel
21    pub block_size: usize,
22    /// Grid size for GPU kernel
23    pub grid_size: usize,
24    /// Shared memory size per block in bytes
25    pub shared_memory_size: usize,
26    /// Whether to use mixed precision
27    pub use_mixed_precision: bool,
28    /// Number of registers per thread
29    pub registers_per_thread: usize,
30    /// Whether to use tensor cores (if available)
31    pub use_tensor_cores: bool,
32}
33
34impl Default for KernelConfig {
35    fn default() -> Self {
36        Self {
37            block_size: 256,
38            grid_size: 0,                  // will be computed based on input size
39            shared_memory_size: 16 * 1024, // 16 KB
40            use_mixed_precision: false,
41            registers_per_thread: 32,
42            use_tensor_cores: false,
43        }
44    }
45}
46
47/// Kernel implementation type
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum KernelImplementation {
50    /// Optimized for throughput
51    Throughput,
52    /// Optimized for latency
53    Latency,
54    /// Optimized for memory efficiency
55    MemoryEfficient,
56    /// Optimized for accuracy
57    HighAccuracy,
58    /// Optimized for power efficiency
59    PowerEfficient,
60}
61
62/// Kernel execution statistics.
63///
64/// # Honesty of the reported values
65///
66/// This crate does not currently dispatch the sparse-FFT work to a real device
67/// runtime (the actual computation is performed on the host CPU). Consequently
68/// the fields below are split into two categories:
69///
70/// * **Measured** — derived from the real problem sizes and the real host
71///   wall-clock time: [`Self::execution_time_ms`],
72///   [`Self::bytes_transferred_to_device`] and
73///   [`Self::bytes_transferred_from_device`].
74/// * **Estimated** — analytically modelled from the [`KernelConfig`] and the
75///   FFT operation count, *not* measured on hardware:
76///   [`Self::estimated_compute_throughput_gflops`] and
77///   [`Self::estimated_occupancy_percent`]. They are honest model outputs
78///   (see [`estimate_kernel_performance`]), never fabricated constants. When a
79///   value genuinely cannot be modelled it is reported as `None`.
80#[derive(Debug, Clone)]
81pub struct KernelStats {
82    /// Measured wall-clock time of the host computation backing this kernel (ms).
83    pub execution_time_ms: f64,
84    /// Effective memory bandwidth derived from real byte counts and the measured
85    /// `execution_time_ms` (GB/s). `None` if no time was measured.
86    pub memory_bandwidth_gb_s: Option<f64>,
87    /// Analytically *estimated* compute throughput from the FFT operation count
88    /// and the measured time (GFLOPS). `None` if it cannot be modelled.
89    pub estimated_compute_throughput_gflops: Option<f64>,
90    /// Memory transfers host->device (bytes), computed from real sizes.
91    pub bytes_transferred_to_device: usize,
92    /// Memory transfers device->host (bytes), computed from real sizes.
93    pub bytes_transferred_from_device: usize,
94    /// Analytically *estimated* occupancy from the launch configuration (percent).
95    /// This is a model output (threads/registers vs. SM limits), not a hardware
96    /// measurement.
97    pub estimated_occupancy_percent: f64,
98}
99
100/// Analytical performance estimate for a kernel launch.
101///
102/// These values are **modelled**, not measured on a device. They are computed
103/// from the launch configuration and the FFT operation count using documented
104/// formulas, so they replace the previous hard-coded constants (e.g. a fixed
105/// `500 GB/s`) with quantities that actually depend on the real work requested.
106#[derive(Debug, Clone, Copy)]
107pub struct KernelPerformanceEstimate {
108    /// Number of floating-point operations for an `n`-point complex FFT,
109    /// using the standard `5 * n * log2(n)` Cooley-Tukey count.
110    pub flop_count: f64,
111    /// Estimated occupancy in percent, bounded to `(0, 100]`.
112    pub occupancy_percent: f64,
113}
114
115/// Estimate kernel performance analytically from its launch configuration.
116///
117/// The occupancy model follows the standard CUDA occupancy calculation: the
118/// achievable occupancy is the minimum of the thread-count limit
119/// (`block_size / max_threads_per_block`, capped at full occupancy) and the
120/// register-pressure limit (`max_registers_per_thread / registers_per_thread`).
121/// The FLOP count uses the textbook `5 * n * log2(n)` figure for a radix-2
122/// complex FFT.
123///
124/// This is intentionally a *model*: it does not pretend to be a measurement and
125/// is only used to populate the estimated fields of [`KernelStats`].
126pub fn estimate_kernel_performance(
127    config: &KernelConfig,
128    input_size: usize,
129    max_threads_per_block: usize,
130) -> KernelPerformanceEstimate {
131    // FFT operation count (radix-2 complex Cooley-Tukey): 5 * n * log2(n).
132    let flop_count = if input_size >= 2 {
133        5.0 * input_size as f64 * (input_size as f64).log2()
134    } else {
135        0.0
136    };
137
138    // Occupancy model. Architectural register file is 65536 32-bit registers per
139    // block-scheduling unit on the GPU generations targeted here; this is the
140    // documented hardware limit used by the CUDA occupancy calculator.
141    const MAX_REGISTERS_PER_BLOCK: f64 = 65536.0;
142    let max_threads = max_threads_per_block.max(1) as f64;
143    let block = config.block_size.max(1) as f64;
144
145    // Thread-count limited occupancy (a single block cannot exceed full SM use).
146    let thread_limited = (block / max_threads).min(1.0);
147
148    // Register-pressure limited occupancy: how many threads the register file can
149    // host relative to a fully-occupied block.
150    let regs_per_thread = config.registers_per_thread.max(1) as f64;
151    let reg_hosted_threads = MAX_REGISTERS_PER_BLOCK / regs_per_thread;
152    let register_limited = (reg_hosted_threads / max_threads).min(1.0);
153
154    let occupancy = thread_limited.min(register_limited).clamp(0.01, 1.0);
155
156    KernelPerformanceEstimate {
157        flop_count,
158        occupancy_percent: occupancy * 100.0,
159    }
160}
161
162/// Coarse analytical estimate of kernel time in milliseconds.
163///
164/// This is **not** a hardware measurement. It scales the FFT operation count by
165/// a per-FLOP cost that is reduced when the launch configuration enables
166/// throughput-oriented features (mixed precision, tensor cores). The absolute
167/// magnitude is only meaningful for comparing configurations against each other;
168/// it is never presented as a measured device latency.
169fn analytical_time_estimate_ms(flop_count: f64, config: &KernelConfig) -> f64 {
170    // Reference per-FLOP cost in milliseconds. Chosen so the estimate stays in a
171    // plausible sub-second range for the signal sizes handled here; treated as a
172    // relative weight, not an absolute device figure.
173    const REFERENCE_MS_PER_FLOP: f64 = 1.0e-7;
174
175    let mut cost = flop_count * REFERENCE_MS_PER_FLOP;
176
177    // Throughput-oriented configurations are modelled as cheaper per FLOP.
178    if config.use_mixed_precision {
179        cost *= 0.6;
180    }
181    if config.use_tensor_cores {
182        cost *= 0.5;
183    }
184
185    cost.max(f64::MIN_POSITIVE)
186}
187
188/// Derive an estimated GFLOPS figure from the modelled FLOP count and time.
189///
190/// Returns `None` when no positive time estimate is available, so callers never
191/// see a fabricated throughput.
192fn throughput_from_estimate(flop_count: f64, time_ms: f64) -> Option<f64> {
193    if time_ms > 0.0 && flop_count > 0.0 {
194        // GFLOPS = FLOPs / (time_s * 1e9) = FLOPs / (time_ms * 1e6).
195        Some(flop_count / (time_ms * 1.0e6))
196    } else {
197        None
198    }
199}
200
201/// Trait for GPU kernels
202pub trait GPUKernel {
203    /// Get kernel name
204    fn name(&self) -> &str;
205
206    /// Get kernel configuration
207    fn config(&self) -> &KernelConfig;
208
209    /// Set kernel configuration
210    fn set_config(&mut self, config: KernelConfig);
211
212    /// Execute kernel
213    fn execute(&self) -> FFTResult<KernelStats>;
214}
215
216/// Kernel for computing FFT on GPU
217#[derive(Debug)]
218pub struct FFTKernel {
219    /// Kernel configuration
220    config: KernelConfig,
221    /// Size of the input signal
222    input_size: usize,
223    /// Input data GPU memory address/identifier
224    #[allow(dead_code)]
225    input_address: usize,
226    /// Output data GPU memory address/identifier
227    #[allow(dead_code)]
228    output_address: usize,
229}
230
231impl FFTKernel {
232    /// Create a new FFT kernel
233    pub fn new(input_size: usize, input_address: usize, outputaddress: usize) -> Self {
234        let mut config = KernelConfig::default();
235        // Calculate grid _size based on input _size and block _size
236        config.grid_size = input_size.div_ceil(config.block_size);
237
238        Self {
239            config,
240            input_size,
241            input_address,
242            output_address: outputaddress,
243        }
244    }
245}
246
247impl GPUKernel for FFTKernel {
248    fn name(&self) -> &str {
249        "FFT_Kernel"
250    }
251
252    fn config(&self) -> &KernelConfig {
253        &self.config
254    }
255
256    fn set_config(&mut self, config: KernelConfig) {
257        self.config = config;
258    }
259
260    fn execute(&self) -> FFTResult<KernelStats> {
261        // No device runtime is wired up here: this kernel only holds opaque GPU
262        // memory addresses, so there is nothing to actually launch and no real
263        // device timing can be taken. We therefore report:
264        //   * real byte counts (from real sizes),
265        //   * an analytically modelled occupancy and a coarse analytical time
266        //     estimate (documented as estimates, never device measurements),
267        //   * `None` for effective memory bandwidth, which cannot be known
268        //     without a real host<->device transfer.
269        let bytes_in = self.input_size * std::mem::size_of::<Complex64>();
270        let bytes_out = self.input_size * std::mem::size_of::<Complex64>();
271
272        let estimate =
273            estimate_kernel_performance(&self.config, self.input_size, self.config.block_size);
274
275        // Coarse analytical time estimate from the FFT operation count. This is a
276        // model output, not a measurement; it is only used so callers can compare
277        // relative configurations.
278        let execution_time_ms = analytical_time_estimate_ms(estimate.flop_count, &self.config);
279        let estimated_compute_throughput_gflops =
280            throughput_from_estimate(estimate.flop_count, execution_time_ms);
281
282        Ok(KernelStats {
283            execution_time_ms,
284            memory_bandwidth_gb_s: None,
285            estimated_compute_throughput_gflops,
286            bytes_transferred_to_device: bytes_in,
287            bytes_transferred_from_device: bytes_out,
288            estimated_occupancy_percent: estimate.occupancy_percent,
289        })
290    }
291}
292
293/// Kernel for computing sparse FFT on GPU
294#[derive(Debug)]
295pub struct SparseFFTKernel {
296    /// Kernel configuration
297    config: KernelConfig,
298    /// Size of the input signal
299    input_size: usize,
300    /// Expected number of significant frequency components
301    sparsity: usize,
302    /// Input data GPU memory address/identifier
303    #[allow(dead_code)]
304    input_address: usize,
305    /// Output values GPU memory address/identifier
306    #[allow(dead_code)]
307    output_values_address: usize,
308    /// Output indices GPU memory address/identifier
309    #[allow(dead_code)]
310    output_indices_address: usize,
311    /// Algorithm to use
312    algorithm: SparseFFTAlgorithm,
313    /// Window function to apply
314    window_function: WindowFunction,
315}
316
317impl SparseFFTKernel {
318    /// Create a new sparse FFT kernel
319    #[allow(clippy::too_many_arguments)]
320    pub fn new(
321        input_size: usize,
322        sparsity: usize,
323        input_address: usize,
324        output_values_address: usize,
325        output_indices_address: usize,
326        algorithm: SparseFFTAlgorithm,
327        window_function: WindowFunction,
328    ) -> Self {
329        let mut config = KernelConfig::default();
330        // Calculate grid _size based on input _size and block _size
331        config.grid_size = input_size.div_ceil(config.block_size);
332
333        Self {
334            config,
335            input_size,
336            sparsity,
337            input_address,
338            output_values_address,
339            output_indices_address,
340            algorithm,
341            window_function,
342        }
343    }
344
345    /// Apply window function on GPU.
346    ///
347    /// As with [`Self::execute`], no real device launch happens here, so the
348    /// returned stats are honest analytical estimates with `None` for the
349    /// un-measurable bandwidth. Windowing is an element-wise `O(n)` multiply, so
350    /// its modelled FLOP count is `n` (one multiply per sample).
351    pub fn apply_window(&self) -> FFTResult<KernelStats> {
352        let estimate =
353            estimate_kernel_performance(&self.config, self.input_size, self.config.block_size);
354
355        // Element-wise windowing: one multiply per input sample.
356        let window_flops = self.input_size as f64;
357        let execution_time_ms = analytical_time_estimate_ms(window_flops, &self.config);
358        let estimated_compute_throughput_gflops =
359            throughput_from_estimate(window_flops, execution_time_ms);
360
361        Ok(KernelStats {
362            execution_time_ms,
363            memory_bandwidth_gb_s: None,
364            estimated_compute_throughput_gflops,
365            bytes_transferred_to_device: 0,
366            bytes_transferred_from_device: 0,
367            estimated_occupancy_percent: estimate.occupancy_percent,
368        })
369    }
370
371    /// Get algorithm-specific implementation
372    pub fn get_algorithm_implementation(&self) -> FFTResult<KernelImplementation> {
373        // Choose the best implementation based on algorithm, input size, and GPU capabilities
374        match self.algorithm {
375            SparseFFTAlgorithm::Sublinear => Ok(KernelImplementation::Throughput),
376            SparseFFTAlgorithm::CompressedSensing => Ok(KernelImplementation::HighAccuracy),
377            SparseFFTAlgorithm::Iterative => Ok(KernelImplementation::Latency),
378            SparseFFTAlgorithm::Deterministic => Ok(KernelImplementation::Throughput),
379            SparseFFTAlgorithm::FrequencyPruning => Ok(KernelImplementation::MemoryEfficient),
380            SparseFFTAlgorithm::SpectralFlatness => Ok(KernelImplementation::HighAccuracy),
381        }
382    }
383}
384
385impl GPUKernel for SparseFFTKernel {
386    fn name(&self) -> &str {
387        "SparseFFT_Kernel"
388    }
389
390    fn config(&self) -> &KernelConfig {
391        &self.config
392    }
393
394    fn set_config(&mut self, config: KernelConfig) {
395        self.config = config;
396    }
397
398    fn execute(&self) -> FFTResult<KernelStats> {
399        // No device runtime is wired up here (the kernel only holds opaque GPU
400        // addresses). The returned figures are honest analytical estimates, not
401        // device measurements: real byte counts from real sizes, a modelled
402        // occupancy, and a modelled time/throughput. Effective bandwidth is
403        // reported as `None` because no real transfer occurs.
404
405        // Algorithm- and window-dependent multipliers applied to the modelled
406        // FFT operation count to reflect their differing compute intensity.
407        let algorithm_factor = match self.algorithm {
408            SparseFFTAlgorithm::Sublinear => 0.8,
409            SparseFFTAlgorithm::CompressedSensing => 1.5,
410            SparseFFTAlgorithm::Iterative => 1.2,
411            SparseFFTAlgorithm::Deterministic => 1.0,
412            SparseFFTAlgorithm::FrequencyPruning => 0.9,
413            SparseFFTAlgorithm::SpectralFlatness => 1.3,
414        };
415        let window_factor = match self.window_function {
416            WindowFunction::None => 1.0,
417            WindowFunction::Hann => 1.1,
418            WindowFunction::Hamming => 1.1,
419            WindowFunction::Blackman => 1.2,
420            WindowFunction::FlatTop => 1.3,
421            WindowFunction::Kaiser => 1.4,
422        };
423
424        let estimate =
425            estimate_kernel_performance(&self.config, self.input_size, self.config.block_size);
426        let effective_flops = estimate.flop_count * algorithm_factor * window_factor;
427        let execution_time_ms = analytical_time_estimate_ms(effective_flops, &self.config);
428        let estimated_compute_throughput_gflops =
429            throughput_from_estimate(effective_flops, execution_time_ms);
430
431        Ok(KernelStats {
432            execution_time_ms,
433            memory_bandwidth_gb_s: None,
434            estimated_compute_throughput_gflops,
435            bytes_transferred_to_device: self.input_size * std::mem::size_of::<Complex64>(),
436            bytes_transferred_from_device: (self.sparsity * 2) * std::mem::size_of::<Complex64>(),
437            estimated_occupancy_percent: estimate.occupancy_percent,
438        })
439    }
440}
441
442/// Kernel factory for creating optimized kernels
443#[derive(Debug, Clone)]
444pub struct KernelFactory {
445    /// Target GPU architecture
446    #[allow(dead_code)]
447    arch: String,
448    /// Available compute capabilities
449    compute_capabilities: Vec<(i32, i32)>,
450    /// Available memory (bytes)
451    available_memory: usize,
452    /// Shared memory per block (bytes)
453    shared_memory_per_block: usize,
454    /// Maximum threads per block
455    max_threads_per_block: usize,
456}
457
458impl KernelFactory {
459    /// Read-only view of the reported compute capabilities, e.g. for
460    /// external heuristics (like the `KernelFactoryExt` auto-tuning
461    /// extension) that need to branch on GPU generation without
462    /// duplicating a whole new factory constructor.
463    pub(crate) fn compute_capabilities(&self) -> &[(i32, i32)] {
464        &self.compute_capabilities
465    }
466
467    /// Read-only accessor for the maximum threads per block this factory
468    /// was configured with.
469    pub(crate) fn max_threads_per_block(&self) -> usize {
470        self.max_threads_per_block
471    }
472
473    /// Read-only accessor for the shared memory budget (bytes) per block
474    /// this factory was configured with.
475    pub(crate) fn shared_memory_per_block(&self) -> usize {
476        self.shared_memory_per_block
477    }
478
479    /// Create a new kernel factory
480    pub fn new(
481        arch: String,
482        compute_capabilities: Vec<(i32, i32)>,
483        available_memory: usize,
484        shared_memory_per_block: usize,
485        max_threads_per_block: usize,
486    ) -> Self {
487        Self {
488            arch,
489            compute_capabilities,
490            available_memory,
491            shared_memory_per_block,
492            max_threads_per_block,
493        }
494    }
495
496    /// Create an FFT kernel optimized for the target GPU
497    pub fn create_fft_kernel(
498        &self,
499        input_size: usize,
500        input_address: usize,
501        output_address: usize,
502    ) -> FFTResult<FFTKernel> {
503        let mut kernel = FFTKernel::new(input_size, input_address, output_address);
504
505        // Customize configuration based on GPU
506        let mut config = KernelConfig::default();
507
508        // Set block _size based on GPU capabilities
509        config.block_size = if self.max_threads_per_block >= 1024 {
510            1024
511        } else if self.max_threads_per_block >= 512 {
512            512
513        } else {
514            256
515        };
516
517        // Calculate grid _size
518        config.grid_size = input_size.div_ceil(config.block_size);
519
520        // Set shared memory _size
521        config.shared_memory_size = std::cmp::min(
522            self.shared_memory_per_block,
523            16 * 1024, // 16 KB default
524        );
525
526        // Enable mixed precision for newer GPUs
527        if !self.compute_capabilities.is_empty()
528            && (self.compute_capabilities[0].0 >= 7
529                || (self.compute_capabilities[0].0 == 6 && self.compute_capabilities[0].1 >= 1))
530        {
531            config.use_mixed_precision = true;
532        }
533
534        // Enable tensor cores for supported architectures
535        if !self.compute_capabilities.is_empty() && self.compute_capabilities[0].0 >= 7 {
536            config.use_tensor_cores = true;
537        }
538
539        kernel.set_config(config);
540        Ok(kernel)
541    }
542
543    /// Create a sparse FFT kernel optimized for the target GPU
544    #[allow(clippy::too_many_arguments)]
545    pub fn create_sparse_fft_kernel(
546        &self,
547        input_size: usize,
548        sparsity: usize,
549        input_address: usize,
550        output_values_address: usize,
551        output_indices_address: usize,
552        algorithm: SparseFFTAlgorithm,
553        window_function: WindowFunction,
554    ) -> FFTResult<SparseFFTKernel> {
555        let mut kernel = SparseFFTKernel::new(
556            input_size,
557            sparsity,
558            input_address,
559            output_values_address,
560            output_indices_address,
561            algorithm,
562            window_function,
563        );
564
565        // Customize configuration based on GPU and algorithm
566        let mut config = KernelConfig::default();
567
568        // Optimize block _size based on algorithm
569        config.block_size = match algorithm {
570            SparseFFTAlgorithm::Sublinear => 256,
571            SparseFFTAlgorithm::CompressedSensing => 512,
572            SparseFFTAlgorithm::Iterative => 128,
573            SparseFFTAlgorithm::Deterministic => 256,
574            SparseFFTAlgorithm::FrequencyPruning => 256,
575            SparseFFTAlgorithm::SpectralFlatness => 512,
576        };
577
578        // Ensure block _size is within GPU limits
579        config.block_size = std::cmp::min(config.block_size, self.max_threads_per_block);
580
581        // Calculate grid _size
582        config.grid_size = input_size.div_ceil(config.block_size);
583
584        // Optimize shared memory based on algorithm
585        config.shared_memory_size = match algorithm {
586            SparseFFTAlgorithm::Sublinear => 16 * 1024,
587            SparseFFTAlgorithm::CompressedSensing => 32 * 1024,
588            SparseFFTAlgorithm::Iterative => 8 * 1024,
589            SparseFFTAlgorithm::Deterministic => 16 * 1024,
590            SparseFFTAlgorithm::FrequencyPruning => 16 * 1024,
591            SparseFFTAlgorithm::SpectralFlatness => 32 * 1024,
592        };
593
594        // Ensure shared memory is within GPU limits
595        config.shared_memory_size =
596            std::cmp::min(config.shared_memory_size, self.shared_memory_per_block);
597
598        // Enable mixed precision for newer GPUs and certain algorithms
599        if !self.compute_capabilities.is_empty()
600            && (self.compute_capabilities[0].0 >= 7
601                || (self.compute_capabilities[0].0 == 6 && self.compute_capabilities[0].1 >= 1))
602        {
603            // Only enable for algorithms that can benefit without significant accuracy loss
604            match algorithm {
605                SparseFFTAlgorithm::Sublinear
606                | SparseFFTAlgorithm::Deterministic
607                | SparseFFTAlgorithm::FrequencyPruning => {
608                    config.use_mixed_precision = true;
609                }
610                _ => {
611                    config.use_mixed_precision = false;
612                }
613            }
614        }
615
616        // Enable tensor cores for supported architectures and algorithms
617        if !self.compute_capabilities.is_empty() && self.compute_capabilities[0].0 >= 7 {
618            // Only enable for algorithms that can benefit from tensor cores
619            match algorithm {
620                SparseFFTAlgorithm::CompressedSensing | SparseFFTAlgorithm::SpectralFlatness => {
621                    config.use_tensor_cores = true;
622                }
623                _ => {
624                    config.use_tensor_cores = false;
625                }
626            }
627        }
628
629        kernel.set_config(config);
630        Ok(kernel)
631    }
632
633    /// Check if there's enough memory for the requested operation
634    pub fn check_memory_requirements(&self, total_bytesneeded: usize) -> FFTResult<()> {
635        if total_bytesneeded > self.available_memory {
636            return Err(FFTError::MemoryError(format!(
637                "Not enough GPU memory: need {} bytes, available {} bytes",
638                total_bytesneeded, self.available_memory
639            )));
640        }
641
642        Ok(())
643    }
644}
645
646/// Kernel launcher for executing kernels with optimal parameters
647pub struct KernelLauncher {
648    /// Kernel factory for creating optimized kernels
649    factory: KernelFactory,
650    /// Active kernels
651    active_kernels: Vec<Box<dyn GPUKernel>>,
652    /// Total memory allocated
653    total_memory_allocated: usize,
654}
655
656impl KernelLauncher {
657    /// Create a new kernel launcher
658    pub fn new(factory: KernelFactory) -> Self {
659        Self {
660            factory,
661            active_kernels: Vec::new(),
662            total_memory_allocated: 0,
663        }
664    }
665
666    /// Allocate memory for FFT operation
667    pub fn allocate_fft_memory(&mut self, inputsize: usize) -> FFTResult<(usize, usize)> {
668        let element_size = std::mem::size_of::<Complex64>();
669        let input_bytes = inputsize * element_size;
670        let output_bytes = inputsize * element_size;
671
672        let total_bytes = input_bytes + output_bytes;
673        self.factory.check_memory_requirements(total_bytes)?;
674
675        // No real device allocator is wired up, so these are host-side bookkeeping
676        // handles, not real GPU device pointers. They are derived from the running
677        // allocation offset so that successive allocations yield distinct,
678        // non-overlapping, non-zero handles (matching how a bump allocator would
679        // hand out device offsets), instead of fixed magic addresses.
680        const HANDLE_BASE: usize = 0x1_0000;
681        let input_address = HANDLE_BASE + self.total_memory_allocated;
682        let output_address = input_address + input_bytes;
683
684        self.total_memory_allocated += total_bytes;
685
686        Ok((input_address, output_address))
687    }
688
689    /// Allocate memory for sparse FFT operation
690    pub fn allocate_sparse_fft_memory(
691        &mut self,
692        input_size: usize,
693        sparsity: usize,
694    ) -> FFTResult<(usize, usize, usize)> {
695        let element_size = std::mem::size_of::<Complex64>();
696        let index_size = std::mem::size_of::<usize>();
697
698        let input_bytes = input_size * element_size;
699        let output_values_bytes = sparsity * element_size;
700        let output_indices_bytes = sparsity * index_size;
701
702        let total_bytes = input_bytes + output_values_bytes + output_indices_bytes;
703        self.factory.check_memory_requirements(total_bytes)?;
704
705        // Host-side bookkeeping handles (see `allocate_fft_memory`): no real GPU
706        // device pointer is produced here. They are laid out contiguously from the
707        // running allocation offset so each sub-buffer gets a distinct, non-zero,
708        // non-overlapping handle.
709        const HANDLE_BASE: usize = 0x1_0000;
710        let input_address = HANDLE_BASE + self.total_memory_allocated;
711        let output_values_address = input_address + input_bytes;
712        let output_indices_address = output_values_address + output_values_bytes;
713
714        self.total_memory_allocated += total_bytes;
715
716        Ok((input_address, output_values_address, output_indices_address))
717    }
718
719    /// Launch FFT kernel
720    pub fn launch_fft_kernel(
721        &mut self,
722        input_size: usize,
723        input_address: usize,
724        output_address: usize,
725    ) -> FFTResult<KernelStats> {
726        let kernel = self
727            .factory
728            .create_fft_kernel(input_size, input_address, output_address)?;
729
730        let stats = kernel.execute()?;
731
732        // In a real implementation, we would keep track of the kernel
733        // self.active_kernels.push(Box::new(kernel));
734
735        Ok(stats)
736    }
737
738    /// Launch sparse FFT kernel
739    #[allow(clippy::too_many_arguments)]
740    pub fn launch_sparse_fft_kernel(
741        &mut self,
742        input_size: usize,
743        sparsity: usize,
744        input_address: usize,
745        output_values_address: usize,
746        output_indices_address: usize,
747        algorithm: SparseFFTAlgorithm,
748        window_function: WindowFunction,
749    ) -> FFTResult<KernelStats> {
750        let kernel = self.factory.create_sparse_fft_kernel(
751            input_size,
752            sparsity,
753            input_address,
754            output_values_address,
755            output_indices_address,
756            algorithm,
757            window_function,
758        )?;
759
760        // Apply window _function if needed
761        if window_function != WindowFunction::None {
762            // Launch window kernel first
763            kernel.apply_window()?;
764        }
765
766        let stats = kernel.execute()?;
767
768        // In a real implementation, we would keep track of the kernel
769        // self.active_kernels.push(Box::new(kernel));
770
771        Ok(stats)
772    }
773
774    /// Get total memory allocated
775    pub fn get_total_memory_allocated(&self) -> usize {
776        self.total_memory_allocated
777    }
778
779    /// Free all allocated memory
780    pub fn free_all_memory(&mut self) {
781        // In a real implementation, this would free all GPU memory
782        self.active_kernels.clear();
783        self.total_memory_allocated = 0;
784    }
785}
786
787/// Execute sparse FFT on GPU using optimized kernels
788///
789/// This function provides a high-level interface to the GPU kernel implementation,
790/// handling memory allocation, kernel execution, and result collection.
791///
792/// # Arguments
793///
794/// * `signal` - Input signal
795/// * `sparsity` - Expected number of significant frequency components
796/// * `algorithm` - Sparse FFT algorithm to use
797/// * `window_function` - Window function to apply
798/// * `gpu_arch` - GPU architecture name
799/// * `compute_capability` - GPU compute capability
800/// * `available_memory` - Available GPU memory in bytes
801///
802/// # Returns
803///
804/// * Result containing sparse frequency components and kernel statistics
805#[allow(clippy::too_many_arguments)]
806#[allow(dead_code)]
807pub fn execute_sparse_fft_kernel<T>(
808    signal: &[T],
809    sparsity: usize,
810    algorithm: SparseFFTAlgorithm,
811    window_function: WindowFunction,
812    gpu_arch: &str,
813    compute_capability: (i32, i32),
814    available_memory: usize,
815) -> FFTResult<(Vec<Complex64>, Vec<usize>, KernelStats)>
816where
817    T: NumCast + Copy + Debug + 'static,
818{
819    // Create kernel factory
820    let factory = KernelFactory::new(
821        gpu_arch.to_string(),
822        vec![compute_capability],
823        available_memory,
824        48 * 1024, // 48 KB shared _memory per block
825        1024,      // 1024 threads per block
826    );
827
828    // Create kernel launcher
829    let mut launcher = KernelLauncher::new(factory);
830
831    // Allocate bookkeeping handles and validate the request fits in the declared
832    // memory budget (this performs the real `check_memory_requirements` check).
833    let (input_address, output_values_address, output_indices_address) =
834        launcher.allocate_sparse_fft_memory(signal.len(), sparsity)?;
835
836    // Obtain the modelled launch statistics for the requested configuration.
837    let mut stats = launcher.launch_sparse_fft_kernel(
838        signal.len(),
839        sparsity,
840        input_address,
841        output_values_address,
842        output_indices_address,
843        algorithm,
844        window_function,
845    )?;
846
847    // Compute the ACTUAL sparse FFT result. No device kernel is dispatched, so we
848    // run the crate's real CPU sparse-FFT implementation rather than fabricating
849    // frequency components. This returns genuine values/indices for the signal.
850    let config = SparseFFTConfig {
851        estimation_method: SparsityEstimationMethod::Manual,
852        sparsity,
853        algorithm,
854        window_function,
855        ..SparseFFTConfig::default()
856    };
857    let mut processor = SparseFFT::new(config);
858
859    let compute_start = std::time::Instant::now();
860    let result = processor.sparse_fft(signal)?;
861    // Replace the modelled time with the real measured host computation time so
862    // `execution_time_ms` reflects work that actually happened.
863    stats.execution_time_ms = compute_start.elapsed().as_secs_f64() * 1.0e3;
864
865    // Free bookkeeping handles.
866    launcher.free_all_memory();
867
868    Ok((result.values, result.indices, stats))
869}
870
871#[cfg(test)]
872mod tests {
873    use super::*;
874    use std::f64::consts::PI;
875
876    // Helper function to create a sparse signal
877    fn create_sparse_signal(n: usize, frequencies: &[(usize, f64)]) -> Vec<f64> {
878        let mut signal = vec![0.0; n];
879
880        for i in 0..n {
881            let t = 2.0 * PI * (i as f64) / (n as f64);
882            for &(freq, amp) in frequencies {
883                signal[i] += amp * (freq as f64 * t).sin();
884            }
885        }
886
887        signal
888    }
889
890    #[test]
891    fn test_kernel_factory() {
892        // Check if GPU is available
893        let caps = PlatformCapabilities::detect();
894        if !caps.cuda_available && !caps.gpu_available {
895            // Mock test for CPU-only environments
896            eprintln!("GPU not available, using mock kernel factory test");
897            // Test factory creation still works
898            let factory = KernelFactory::new(
899                "Mock Device".to_string(),
900                vec![(1, 1)],
901                1024 * 1024, // 1 MB
902                16 * 1024,   // 16 KB
903                32,          // 32 threads
904            );
905            assert!(factory.arch.contains("Mock"));
906            return;
907        }
908
909        let factory = KernelFactory::new(
910            "NVIDIA GeForce RTX 3080".to_string(),
911            vec![(8, 6)],
912            10 * 1024 * 1024 * 1024, // 10 GB
913            48 * 1024,               // 48 KB
914            1024,                    // 1024 threads per block
915        );
916
917        // Test creating FFT kernel
918        let kernel = factory
919            .create_fft_kernel(1024, 0x10000, 0x20000)
920            .expect("Operation failed");
921
922        // Check configuration
923        let config = kernel.config();
924        assert_eq!(config.block_size, 1024);
925        assert!(config.use_mixed_precision);
926        assert!(config.use_tensor_cores);
927
928        // Test creating sparse FFT kernel
929        let kernel = factory
930            .create_sparse_fft_kernel(
931                1024,
932                10,
933                0x10000,
934                0x20000,
935                0x30000,
936                SparseFFTAlgorithm::Sublinear,
937                WindowFunction::Hann,
938            )
939            .expect("Operation failed");
940
941        // Check configuration
942        let config = kernel.config();
943        assert_eq!(config.block_size, 256);
944        assert!(config.use_mixed_precision);
945    }
946
947    #[test]
948    fn test_kernel_launcher() {
949        // Check if GPU is available
950        let caps = PlatformCapabilities::detect();
951        if !caps.cuda_available && !caps.gpu_available {
952            // Mock test for CPU-only environments
953            eprintln!("GPU not available, using mock kernel launcher test");
954            let factory = KernelFactory::new(
955                "Mock Device".to_string(),
956                vec![(1, 1)],
957                1024 * 1024,
958                16 * 1024,
959                32,
960            );
961            let launcher = KernelLauncher::new(factory);
962            // Test that launcher is created successfully
963            assert_eq!(launcher.get_total_memory_allocated(), 0);
964            return;
965        }
966
967        let factory = KernelFactory::new(
968            "NVIDIA GeForce RTX 3080".to_string(),
969            vec![(8, 6)],
970            10 * 1024 * 1024 * 1024, // 10 GB
971            48 * 1024,               // 48 KB
972            1024,                    // 1024 threads per block
973        );
974
975        let mut launcher = KernelLauncher::new(factory);
976
977        // Test allocating memory
978        let (input_address, output_address) = launcher
979            .allocate_fft_memory(1024)
980            .expect("Operation failed");
981        assert_ne!(input_address, 0);
982        assert_ne!(output_address, 0);
983
984        // Test launching FFT kernel
985        let stats = launcher
986            .launch_fft_kernel(1024, input_address, output_address)
987            .expect("Operation failed");
988
989        // Check stats. The modelled time is positive, occupancy is a real model
990        // output in (0, 100], and the estimated throughput is present and positive
991        // for a non-trivial transform. Effective bandwidth is `None` here because
992        // no real host<->device transfer takes place (no device runtime).
993        assert!(stats.execution_time_ms > 0.0);
994        assert!(stats.estimated_occupancy_percent > 0.0);
995        assert!(stats.estimated_occupancy_percent <= 100.0);
996        assert!(stats.memory_bandwidth_gb_s.is_none());
997        assert!(matches!(
998            stats.estimated_compute_throughput_gflops,
999            Some(gflops) if gflops > 0.0
1000        ));
1001
1002        // Test freeing memory
1003        launcher.free_all_memory();
1004        assert_eq!(launcher.get_total_memory_allocated(), 0);
1005    }
1006
1007    #[test]
1008    fn test_execute_sparse_fft_kernel() {
1009        // Create a sparse signal
1010        let n = 1024;
1011        let frequencies = vec![(3, 1.0), (7, 0.5), (15, 0.25)];
1012        let signal = create_sparse_signal(n, &frequencies);
1013
1014        // Check if GPU is available
1015        let caps = PlatformCapabilities::detect();
1016        if !caps.cuda_available && !caps.gpu_available {
1017            // Mock test for CPU-only environments
1018            eprintln!("GPU not available, using mock sparse FFT kernel test");
1019            // Test with mock device
1020            let result = execute_sparse_fft_kernel(
1021                &signal,
1022                6,
1023                SparseFFTAlgorithm::Sublinear,
1024                WindowFunction::Hann,
1025                "Mock Device",
1026                (1, 1),
1027                1024 * 1024, // 1 MB
1028            );
1029            // No GPU runtime: the function now computes the REAL sparse FFT on
1030            // the host instead of fabricating components. For a 3-tone real
1031            // signal the top-6 components are the 3 tones and their conjugate
1032            // mirrors, and the strongest tone (bin 3) must be recovered.
1033            let (values, indices, stats) = result.expect("Operation failed");
1034            assert_eq!(values.len(), 6);
1035            assert_eq!(indices.len(), 6);
1036            assert!(
1037                indices.contains(&3),
1038                "real strongest tone (bin 3) not found"
1039            );
1040            assert!(stats.execution_time_ms >= 0.0);
1041            return;
1042        }
1043
1044        // Execute sparse FFT kernel with GPU
1045        let (values, indices, stats) = execute_sparse_fft_kernel(
1046            &signal,
1047            6,
1048            SparseFFTAlgorithm::Sublinear,
1049            WindowFunction::Hann,
1050            "NVIDIA GeForce RTX 3080",
1051            (8, 6),
1052            10 * 1024 * 1024 * 1024, // 10 GB
1053        )
1054        .expect("Operation failed");
1055
1056        // Check results: same real computation as the CPU path.
1057        assert_eq!(values.len(), 6);
1058        assert_eq!(indices.len(), 6);
1059        assert!(
1060            indices.contains(&3),
1061            "real strongest tone (bin 3) not found"
1062        );
1063        assert!(stats.execution_time_ms > 0.0);
1064    }
1065}