Skip to main content

ringkernel_accnet/cuda/
runtime.rs

1//! GPU runtime with CUDA acceleration and CPU fallback.
2//!
3//! This module provides a unified runtime interface that uses
4//! CUDA GPU acceleration when available, falling back to CPU execution.
5
6use crate::kernels::{AnalysisConfig, AnalysisKernel, AnalysisResult};
7use crate::models::AccountingNetwork;
8
9/// Backend selection for kernel execution.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum Backend {
12    /// Automatically select best available backend.
13    #[default]
14    Auto,
15    /// Force CUDA GPU execution.
16    Cuda,
17    /// Force CPU execution.
18    Cpu,
19}
20
21/// Runtime status and capabilities.
22#[derive(Debug, Clone)]
23pub struct RuntimeStatus {
24    /// Active backend.
25    pub backend: Backend,
26    /// Whether CUDA is available.
27    pub cuda_available: bool,
28    /// CUDA device name (if available).
29    pub cuda_device_name: Option<String>,
30    /// CUDA compute capability (if available).
31    pub cuda_compute_capability: Option<(u32, u32)>,
32    /// Whether GPU kernels are compiled and ready.
33    pub gpu_kernels_ready: bool,
34}
35
36impl Default for RuntimeStatus {
37    fn default() -> Self {
38        Self {
39            backend: Backend::Cpu,
40            cuda_available: false,
41            cuda_device_name: None,
42            cuda_compute_capability: None,
43            gpu_kernels_ready: false,
44        }
45    }
46}
47
48/// GPU-accelerated analysis runtime.
49///
50/// Uses CUDA for analysis kernels when available, with automatic fallback to CPU.
51pub struct AnalysisRuntime {
52    /// Selected backend.
53    backend: Backend,
54    /// CPU kernel (always available).
55    cpu_kernel: AnalysisKernel,
56    /// Runtime status.
57    status: RuntimeStatus,
58    /// GPU executor (when cuda feature enabled and CUDA available).
59    #[cfg(feature = "cuda")]
60    gpu_executor: Option<super::executor::GpuExecutor>,
61}
62
63impl AnalysisRuntime {
64    /// Create a new runtime with automatic backend selection.
65    pub fn new() -> Self {
66        Self::with_backend(Backend::Auto)
67    }
68
69    /// Create a runtime with a specific backend preference.
70    pub fn with_backend(backend: Backend) -> Self {
71        let cpu_kernel = AnalysisKernel::new(AnalysisConfig::default());
72        let mut status = RuntimeStatus::default();
73
74        // Try to initialize GPU executor
75        #[cfg(feature = "cuda")]
76        let (gpu_executor, cuda_available) = Self::try_init_gpu(&mut status);
77
78        #[cfg(not(feature = "cuda"))]
79        let cuda_available = false;
80
81        // Determine actual backend
82        let active_backend = match backend {
83            Backend::Auto => {
84                if cuda_available {
85                    Backend::Cuda
86                } else {
87                    Backend::Cpu
88                }
89            }
90            Backend::Cuda => {
91                if cuda_available {
92                    Backend::Cuda
93                } else {
94                    eprintln!("Warning: CUDA requested but not available, falling back to CPU");
95                    Backend::Cpu
96                }
97            }
98            Backend::Cpu => Backend::Cpu,
99        };
100
101        status.backend = active_backend;
102        status.cuda_available = cuda_available;
103
104        Self {
105            backend: active_backend,
106            cpu_kernel,
107            status,
108            #[cfg(feature = "cuda")]
109            gpu_executor,
110        }
111    }
112
113    /// Try to initialize GPU executor.
114    #[cfg(feature = "cuda")]
115    fn try_init_gpu(status: &mut RuntimeStatus) -> (Option<super::executor::GpuExecutor>, bool) {
116        // Check if CUDA is available
117        if !ringkernel_cuda::is_cuda_available() {
118            return (None, false);
119        }
120
121        // Try to create GPU executor
122        match super::executor::GpuExecutor::new() {
123            Ok(mut executor) => {
124                status.cuda_device_name = Some(executor.device_name().to_string());
125                status.cuda_compute_capability = Some(executor.compute_capability());
126
127                // Try to compile kernels
128                match executor.compile_kernels() {
129                    Ok(()) => {
130                        status.gpu_kernels_ready = true;
131                        eprintln!(
132                            "GPU: {} (CC {}.{}) - Kernels compiled",
133                            executor.device_name(),
134                            executor.compute_capability().0,
135                            executor.compute_capability().1
136                        );
137                        (Some(executor), true)
138                    }
139                    Err(e) => {
140                        eprintln!("Warning: Failed to compile GPU kernels: {}", e);
141                        (None, false)
142                    }
143                }
144            }
145            Err(e) => {
146                eprintln!("Warning: Failed to initialize GPU: {}", e);
147                (None, false)
148            }
149        }
150    }
151
152    /// Get the active backend.
153    pub fn backend(&self) -> Backend {
154        self.backend
155    }
156
157    /// Get runtime status.
158    pub fn status(&self) -> &RuntimeStatus {
159        &self.status
160    }
161
162    /// Check if CUDA is being used.
163    pub fn is_cuda_active(&self) -> bool {
164        self.backend == Backend::Cuda && self.status.gpu_kernels_ready
165    }
166
167    /// Analyze the network using the best available backend.
168    pub fn analyze(&self, network: &AccountingNetwork) -> AnalysisResult {
169        #[cfg(feature = "cuda")]
170        {
171            if self.is_cuda_active() {
172                if let Some(ref executor) = self.gpu_executor {
173                    match executor.analyze(network) {
174                        Ok(gpu_result) => {
175                            // Convert GPU result to AnalysisResult format
176                            return self.convert_gpu_result(network, gpu_result);
177                        }
178                        Err(e) => {
179                            eprintln!("GPU analysis failed, falling back to CPU: {}", e);
180                        }
181                    }
182                }
183            }
184        }
185
186        // Fallback to CPU
187        self.cpu_kernel.analyze(network)
188    }
189
190    /// Convert GPU result to standard AnalysisResult format.
191    #[cfg(feature = "cuda")]
192    fn convert_gpu_result(
193        &self,
194        network: &AccountingNetwork,
195        gpu_result: super::executor::GpuAnalysisResult,
196    ) -> AnalysisResult {
197        use crate::kernels::AnalysisStats;
198        use crate::models::{GaapViolation, GaapViolationType, HybridTimestamp, ViolationSeverity};
199
200        // Build suspense results as (index, score) tuples
201        let suspense_accounts: Vec<(u16, f32)> = gpu_result
202            .suspense_scores
203            .iter()
204            .enumerate()
205            .filter(|(_, &score)| score > 0.5)
206            .map(|(idx, &score)| (idx as u16, score))
207            .collect();
208
209        // Build GAAP violation results
210        let gaap_violations: Vec<GaapViolation> = gpu_result
211            .gaap_violations
212            .iter()
213            .enumerate()
214            .filter(|(_, &flag)| flag > 0)
215            .map(|(idx, &flag)| {
216                let flow = network.flows.get(idx);
217                GaapViolation {
218                    id: uuid::Uuid::new_v4(),
219                    violation_type: match flag {
220                        1 => GaapViolationType::RevenueToCashDirect,
221                        2 => GaapViolationType::RevenueToExpense,
222                        _ => GaapViolationType::UnbalancedEntry,
223                    },
224                    severity: if flag == 1 {
225                        ViolationSeverity::High
226                    } else {
227                        ViolationSeverity::Medium
228                    },
229                    source_account: flow.map(|f| f.source_account_index).unwrap_or(0),
230                    target_account: flow.map(|f| f.target_account_index).unwrap_or(0),
231                    amount: flow.map(|f| f.amount).unwrap_or_default(),
232                    journal_entry_id: uuid::Uuid::nil(),
233                    detected_at: HybridTimestamp::now(),
234                    description: match flag {
235                        1 => "Revenue to Asset Direct (GPU detected)".to_string(),
236                        2 => "Revenue to Expense Direct (GPU detected)".to_string(),
237                        _ => "Unknown violation".to_string(),
238                    },
239                }
240            })
241            .collect();
242
243        AnalysisResult {
244            stats: AnalysisStats {
245                accounts_analyzed: network.accounts.len(),
246                flows_analyzed: network.flows.len(),
247                suspense_count: suspense_accounts.len(),
248                gaap_violation_count: gaap_violations.len(),
249                fraud_pattern_count: 0,
250            },
251            suspense_accounts,
252            gaap_violations,
253            fraud_patterns: Vec::new(), // Not implemented in GPU yet
254        }
255    }
256
257    /// Run benchmarks comparing CPU vs GPU performance.
258    #[cfg(feature = "cuda")]
259    pub fn run_benchmarks(
260        &self,
261        network: &AccountingNetwork,
262    ) -> Option<super::executor::BenchmarkResults> {
263        if let Some(ref executor) = self.gpu_executor {
264            match executor.run_benchmarks(network) {
265                Ok(results) => Some(results),
266                Err(e) => {
267                    eprintln!("Benchmark failed: {}", e);
268                    None
269                }
270            }
271        } else {
272            None
273        }
274    }
275
276    /// Get the generated CUDA kernel code (for inspection/debugging).
277    #[cfg(feature = "cuda")]
278    pub fn cuda_kernel_code(&self, kernel_type: super::CudaKernelType) -> Option<String> {
279        super::GeneratedKernels::generate()
280            .ok()
281            .map(|k| match kernel_type {
282                super::CudaKernelType::SuspenseDetection => k.suspense_detection,
283                super::CudaKernelType::GaapViolation => k.gaap_violation,
284                super::CudaKernelType::BenfordAnalysis => k.benford_analysis,
285                _ => String::new(),
286            })
287    }
288}
289
290impl Default for AnalysisRuntime {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use uuid::Uuid;
300
301    #[test]
302    fn test_runtime_creation() {
303        let runtime = AnalysisRuntime::new();
304        // Should work regardless of CUDA availability
305        assert!(runtime.backend() == Backend::Cpu || runtime.backend() == Backend::Cuda);
306    }
307
308    #[test]
309    fn test_cpu_fallback() {
310        let runtime = AnalysisRuntime::with_backend(Backend::Cpu);
311        assert_eq!(runtime.backend(), Backend::Cpu);
312    }
313
314    #[test]
315    fn test_analysis() {
316        let runtime = AnalysisRuntime::new();
317        let network = AccountingNetwork::new(Uuid::new_v4(), 2024, 1);
318        let result = runtime.analyze(&network);
319        assert_eq!(result.stats.accounts_analyzed, network.accounts.len());
320    }
321}