Skip to main content

rfann/webgpu/
compute_context.rs

1//! ComputeContext bridge for `Network<T>` integration with advanced WebGPU backend
2//!
3//! This module provides a bridge between the existing `Network<T>` structure and the
4//! advanced WebGPU backend, enabling seamless GPU acceleration.
5
6use num_traits::Float;
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use crate::webgpu::{
11    backend::{BackendSelector, BackendType, ComputeBackend},
12    error::{ComputeError, ComputeResult},
13};
14use crate::{ActivationFunction, Layer, Network};
15
16#[cfg(feature = "gpu")]
17use crate::webgpu::webgpu_backend::WebGPUBackend;
18
19// These types are used across the module regardless of webgpu feature
20#[derive(Clone, Copy, Debug)]
21pub struct MatrixDims {
22    pub rows: usize,
23    pub cols: usize,
24}
25
26#[derive(Clone, Debug)]
27pub struct DeviceCapabilities {
28    pub max_buffer_size: u64,
29    pub max_workgroup_size: usize,
30    pub shared_memory_size: u64,
31}
32
33#[derive(Clone, Debug, Default)]
34pub struct PerformanceStats {
35    pub kernel_time_ms: f64,
36    pub memory_transfer_ms: f64,
37    pub total_time_ms: f64,
38}
39
40/// ComputeContext manages backend selection and operation dispatch for `Network<T>`
41///
42/// This bridge provides seamless integration between `Network<T>` and the advanced
43/// WebGPU backend while maintaining full compatibility with existing code.
44#[derive(Debug)]
45pub struct ComputeContext<T: Float + std::fmt::Debug + Send + Sync + 'static> {
46    /// Backend selector for intelligent backend switching
47    backend_selector: BackendSelector<T>,
48    /// Current backend type being used
49    current_backend: BackendType,
50    /// WebGPU backend instance (when available)
51    #[cfg(feature = "gpu")]
52    webgpu_backend: Option<Arc<WebGPUBackend<T>>>,
53    #[cfg(not(feature = "gpu"))]
54    webgpu_backend: Option<()>,
55    /// GPU acceleration enabled flag
56    gpu_enabled: bool,
57    /// Performance tracking and optimization
58    performance_tracker: Arc<std::sync::Mutex<PerformanceTracker>>,
59    /// Cache for converted weights to avoid repeated conversions
60    weight_cache: std::collections::HashMap<usize, (Vec<T>, MatrixDims)>,
61}
62
63/// Performance tracking for optimization decisions
64#[derive(Debug)]
65struct PerformanceTracker {
66    operation_counts: HashMap<String, u64>,
67    execution_times: HashMap<String, Vec<f64>>,
68    backend_switches: HashMap<BackendType, u64>,
69    optimization_events: Vec<OptimizationEvent>,
70}
71
72#[derive(Debug, Clone)]
73struct OptimizationEvent {
74    timestamp: std::time::Instant,
75    event_type: String,
76    backend_from: BackendType,
77    backend_to: BackendType,
78    performance_gain: f64,
79}
80
81impl<T: Float + Send + Sync + std::fmt::Debug + 'static> ComputeContext<T> {
82    /// Create a new compute context with automatic backend detection
83    pub fn new() -> ComputeResult<Self> {
84        let backend_selector = BackendSelector::new();
85
86        // Try to initialize WebGPU backend
87        #[cfg(feature = "gpu")]
88        let (webgpu_backend, gpu_enabled) = {
89            // Use a simple sync check for GPU availability
90            let gpu_available = cfg!(feature = "gpu");
91            if gpu_available {
92                // For now, assume GPU is available if feature is enabled
93                // In a real implementation, we'd do proper GPU detection
94                (None, false) // Set to false for now to avoid async complications
95            } else {
96                (None, false)
97            }
98        };
99
100        #[cfg(not(feature = "gpu"))]
101        let (webgpu_backend, gpu_enabled) = (None, false);
102
103        // Select initial backend based on availability
104        let current_backend = if gpu_enabled {
105            BackendType::WebGPU
106        } else {
107            BackendType::Simd
108        };
109
110        Ok(Self {
111            backend_selector,
112            current_backend,
113            webgpu_backend,
114            gpu_enabled,
115            performance_tracker: Arc::new(std::sync::Mutex::new(PerformanceTracker::new())),
116            weight_cache: HashMap::new(),
117        })
118    }
119
120    /// Create a compute context with CPU-only backend (for testing/fallback)
121    pub fn cpu_only() -> Self {
122        Self {
123            backend_selector: BackendSelector::new(),
124            current_backend: BackendType::Cpu,
125            webgpu_backend: None,
126            gpu_enabled: false,
127            performance_tracker: Arc::new(std::sync::Mutex::new(PerformanceTracker::new())),
128            weight_cache: HashMap::new(),
129        }
130    }
131
132    /// Check if GPU acceleration is available
133    pub fn is_gpu_available(&self) -> bool {
134        self.gpu_enabled && self.webgpu_backend.is_some()
135    }
136
137    /// Get current backend type
138    pub fn current_backend(&self) -> BackendType {
139        self.current_backend
140    }
141
142    /// Select optimal backend for given problem size
143    pub fn select_backend(&mut self, problem_size: usize) -> BackendType {
144        let profile = crate::webgpu::backend::ComputeProfile {
145            matrix_size: match problem_size {
146                0..=10000 => crate::webgpu::backend::MatrixSize::Small,
147                10001..=1000000 => crate::webgpu::backend::MatrixSize::Medium,
148                _ => crate::webgpu::backend::MatrixSize::Large,
149            },
150            batch_size: 1,
151            operation_type: crate::webgpu::backend::OperationType::Inference,
152        };
153
154        let selected = self
155            .backend_selector
156            .select_backend(&profile)
157            .map(|backend| backend.backend_type())
158            .unwrap_or(BackendType::Cpu);
159
160        // Only use GPU if it's actually available
161        if selected == BackendType::WebGPU && !self.is_gpu_available() {
162            self.current_backend = BackendType::Simd;
163        } else {
164            self.current_backend = selected;
165        }
166
167        self.current_backend
168    }
169
170    /// Convert Network layer to matrix format with caching
171    fn get_layer_weights(
172        &mut self,
173        layer: &Layer<T>,
174        layer_id: usize,
175    ) -> ComputeResult<(Vec<T>, MatrixDims)> {
176        // Check cache first
177        if let Some(cached) = self.weight_cache.get(&layer_id) {
178            return Ok(cached.clone());
179        }
180
181        // Debug layer information
182        println!("Converting layer {layer_id} to matrix format");
183        println!("  Layer has {} neurons", layer.neurons.len());
184
185        // In FANN networks, bias neurons are included in the layer
186        // We need to find non-bias neurons for the output
187        let non_bias_neurons: Vec<&crate::Neuron<T>> =
188            layer.neurons.iter().filter(|n| !n.is_bias).collect();
189
190        println!("  Layer has {} non-bias neurons", non_bias_neurons.len());
191
192        // Convert layer connections to matrix format
193        // In a FANN network, the input size is the number of connections on each neuron
194        // (all neurons should have the same number of connections)
195        let input_size = if let Some(neuron) = non_bias_neurons.first() {
196            println!(
197                "  First neuron has {} connections",
198                neuron.connections.len()
199            );
200            neuron.connections.len()
201        } else {
202            println!("  No non-bias neurons found!");
203            return Err(ComputeError::InvalidDimensions(format!(
204                "Layer {layer_id} has no non-bias neurons"
205            )));
206        };
207
208        let output_size = non_bias_neurons.len();
209
210        println!("  Matrix dimensions: {output_size}x{input_size} (output_size x input_size)");
211
212        if input_size == 0 || output_size == 0 {
213            return Err(ComputeError::InvalidDimensions(format!(
214                "Invalid layer dimensions: {output_size}x{input_size}"
215            )));
216        }
217
218        let mut weights = Vec::with_capacity(output_size * input_size);
219
220        // Build weight matrix row by row (each row = one output neuron's weights)
221        for neuron in &non_bias_neurons {
222            // Ensure we have enough connections
223            if neuron.connections.len() != input_size {
224                return Err(ComputeError::InvalidDimensions(format!(
225                    "Neuron has {} connections, expected {}",
226                    neuron.connections.len(),
227                    input_size
228                )));
229            }
230
231            // Add weights for this neuron to the matrix
232            for i in 0..input_size {
233                weights.push(neuron.connections[i].weight);
234            }
235        }
236
237        if weights.len() != output_size * input_size {
238            return Err(ComputeError::InvalidDimensions(format!(
239                "Weight matrix size mismatch: got {}, expected {}",
240                weights.len(),
241                output_size * input_size
242            )));
243        }
244
245        let dims = MatrixDims {
246            rows: output_size,
247            cols: input_size,
248        };
249        let result = (weights, dims);
250
251        // Cache the result
252        self.weight_cache.insert(layer_id, result.clone());
253
254        Ok(result)
255    }
256
257    /// Execute forward pass for a layer with optimal backend selection
258    pub async fn compute_layer_forward(
259        &mut self,
260        layer: &Layer<T>,
261        layer_id: usize,
262        inputs: &[T],
263    ) -> ComputeResult<Vec<T>>
264    where
265        T: Clone + num_traits::ToPrimitive + 'static,
266    {
267        let start_time = std::time::Instant::now();
268
269        // Get layer weights
270        let (weights, dims) = self.get_layer_weights(layer, layer_id)?;
271
272        // Check if we need to append a bias input (value 1.0)
273        let mut input_with_bias = inputs.to_vec();
274        if dims.cols == inputs.len() + 1 {
275            // The extra column is likely for the bias input (common in FANN architecture)
276            println!("  Adding bias input to match expected dimensions");
277            input_with_bias.push(T::one()); // Add bias input with value 1.0
278        } else if inputs.len() != dims.cols {
279            return Err(ComputeError::InvalidDimensions(format!(
280                "Input size {} doesn't match expected {} and doesn't match bias pattern",
281                inputs.len(),
282                dims.cols
283            )));
284        }
285
286        // Select optimal backend for this problem size
287        let problem_size = dims.rows * dims.cols;
288        let backend_type = self.select_backend(problem_size);
289
290        // Execute computation based on selected backend
291        let result = match backend_type {
292            BackendType::WebGPU if self.is_gpu_available() => {
293                self.compute_layer_gpu(layer, &weights, &input_with_bias, dims)
294                    .await
295            }
296            BackendType::Simd => {
297                self.compute_layer_simd(layer, &weights, &input_with_bias, dims)
298                    .await
299            }
300            _ => {
301                self.compute_layer_cpu(layer, &weights, &input_with_bias, dims)
302                    .await
303            }
304        };
305
306        // Record performance metrics
307        let duration = start_time.elapsed().as_secs_f64();
308        if let Ok(mut tracker) = self.performance_tracker.lock() {
309            tracker.record_operation("layer_forward", duration, backend_type);
310        }
311
312        result
313    }
314
315    /// GPU-accelerated layer computation
316    async fn compute_layer_gpu(
317        &self,
318        layer: &Layer<T>,
319        weights: &[T],
320        inputs: &[T],
321        dims: MatrixDims,
322    ) -> ComputeResult<Vec<T>>
323    where
324        T: Clone + num_traits::ToPrimitive + 'static,
325    {
326        #[cfg(feature = "gpu")]
327        {
328            if let Some(ref gpu_backend) = self.webgpu_backend {
329                // Matrix-vector multiplication
330                let outputs =
331                    gpu_backend.matrix_vector_multiply(weights, inputs, dims.rows, dims.cols)?;
332
333                // Apply activation function
334                // Get activation function from first non-bias neuron
335                let activation_function = layer
336                    .neurons
337                    .iter()
338                    .find(|n| !n.is_bias)
339                    .map(|n| n.activation_function)
340                    .unwrap_or(ActivationFunction::Linear);
341                let steepness = T::one();
342                gpu_backend.apply_activation_function(&outputs, activation_function, steepness)
343            } else {
344                Err(ComputeError::GpuUnavailable)
345            }
346        }
347
348        #[cfg(not(feature = "gpu"))]
349        {
350            Err(ComputeError::GpuUnavailable)
351        }
352    }
353
354    /// SIMD-optimized layer computation
355    async fn compute_layer_simd(
356        &self,
357        layer: &Layer<T>,
358        weights: &[T],
359        inputs: &[T],
360        dims: MatrixDims,
361    ) -> ComputeResult<Vec<T>>
362    where
363        T: Clone + 'static,
364    {
365        // Use backend selector to get SIMD backend
366        let profile = crate::webgpu::backend::ComputeProfile {
367            matrix_size: crate::webgpu::backend::MatrixSize::Medium,
368            batch_size: 1,
369            operation_type: crate::webgpu::backend::OperationType::Inference,
370        };
371
372        if let Some(backend) = self.backend_selector.select_backend(&profile) {
373            let outputs = backend.matrix_vector_multiply(weights, inputs, dims.rows, dims.cols)?;
374            // Get activation function from first non-bias neuron
375            let activation_function = layer
376                .neurons
377                .iter()
378                .find(|n| !n.is_bias)
379                .map(|n| n.activation_function)
380                .unwrap_or(ActivationFunction::Linear);
381            let steepness = T::one();
382            backend.apply_activation_function(&outputs, activation_function, steepness)
383        } else {
384            self.compute_layer_cpu(layer, weights, inputs, dims).await
385        }
386    }
387
388    /// CPU fallback layer computation
389    async fn compute_layer_cpu(
390        &self,
391        layer: &Layer<T>,
392        weights: &[T],
393        inputs: &[T],
394        dims: MatrixDims,
395    ) -> ComputeResult<Vec<T>> {
396        let mut outputs = Vec::with_capacity(dims.rows);
397
398        // Manual matrix-vector multiplication
399        for row in 0..dims.rows {
400            let mut sum = T::zero();
401            for col in 0..dims.cols {
402                sum = sum + weights[row * dims.cols + col] * inputs[col];
403            }
404            outputs.push(sum);
405        }
406
407        // Apply activation function
408        // Get activation function from first non-bias neuron
409        let activation_function = layer
410            .neurons
411            .iter()
412            .find(|n| !n.is_bias)
413            .map(|n| n.activation_function)
414            .unwrap_or(ActivationFunction::Linear);
415        let result: Vec<T> = outputs
416            .into_iter()
417            .map(|x| apply_activation_cpu(x, activation_function, T::one()))
418            .collect();
419
420        Ok(result)
421    }
422
423    /// Execute complete network forward pass with optimal backend coordination
424    pub async fn compute_network_forward(
425        &mut self,
426        network: &Network<T>,
427        inputs: &[T],
428    ) -> ComputeResult<Vec<T>>
429    where
430        T: Clone + num_traits::ToPrimitive + 'static,
431    {
432        // Validate network has layers
433        if network.layers.is_empty() {
434            return Err(ComputeError::InvalidDimensions(
435                "Network has no layers".to_string(),
436            ));
437        }
438
439        // Validate input size matches input layer (excluding bias neuron)
440        if !network.layers.is_empty() && inputs.len() != network.num_inputs() {
441            return Err(ComputeError::InvalidDimensions(format!(
442                "Input size {} doesn't match network input size {}",
443                inputs.len(),
444                network.num_inputs()
445            )));
446        }
447
448        let mut current_inputs = inputs.to_vec();
449
450        // Process each layer, starting from the first hidden layer (index 1)
451        // The input layer (index 0) is just for passing inputs
452        for (layer_id, layer) in network.layers.iter().enumerate().skip(1) {
453            // Skip input layer (index 0)
454            current_inputs = match self
455                .compute_layer_forward(layer, layer_id, &current_inputs)
456                .await
457            {
458                Ok(outputs) => outputs,
459                Err(e) => {
460                    eprintln!("Error in layer {layer_id}: {e:?}");
461                    return Err(e);
462                }
463            };
464        }
465
466        Ok(current_inputs)
467    }
468
469    /// Clear weight cache (call when network weights change)
470    pub fn clear_cache(&mut self) {
471        self.weight_cache.clear();
472    }
473
474    /// Get comprehensive performance statistics
475    pub fn get_performance_stats(&self) -> ComputePerformanceStats {
476        let tracker_stats = if let Ok(tracker) = self.performance_tracker.lock() {
477            Some(tracker.get_stats())
478        } else {
479            None
480        };
481
482        #[cfg(feature = "gpu")]
483        let gpu_stats = self.webgpu_backend.as_ref().map(|_gpu_backend| {
484            // TODO: Implement get_performance_stats in WebGPUBackend
485            PerformanceStats::default()
486        });
487
488        #[cfg(not(feature = "gpu"))]
489        let gpu_stats = None;
490
491        ComputePerformanceStats {
492            current_backend: self.current_backend,
493            gpu_available: self.is_gpu_available(),
494            cache_size: self.weight_cache.len(),
495            tracker_stats,
496            gpu_stats,
497        }
498    }
499
500    /// Get memory manager for GPU buffer operations
501    pub fn memory_manager(&self) -> GpuMemoryManager<T> {
502        GpuMemoryManager::new()
503    }
504}
505
506/// GPU memory manager for training operations
507pub struct GpuMemoryManager<T: Float> {
508    _phantom: std::marker::PhantomData<T>,
509}
510
511impl<T: Float> GpuMemoryManager<T> {
512    /// Create a new GPU memory manager
513    pub fn new() -> Self {
514        Self {
515            _phantom: std::marker::PhantomData,
516        }
517    }
518
519    /// Allocate a GPU buffer
520    pub fn allocate_buffer(&self, size: usize) -> ComputeResult<super::memory::BufferHandle> {
521        // TODO: Implement actual GPU buffer allocation
522        // For now, return a placeholder handle
523        Ok(super::memory::BufferHandle::new(size as u64))
524    }
525
526    /// Upload data to GPU buffer
527    pub fn upload_data(
528        &self,
529        _handle: super::memory::BufferHandle,
530        _data: &[T],
531    ) -> ComputeResult<()> {
532        // TODO: Implement GPU data upload
533        Ok(())
534    }
535
536    /// Download data from GPU buffer
537    pub fn download_data(&self, _handle: super::memory::BufferHandle) -> ComputeResult<Vec<T>> {
538        // TODO: Implement GPU data download
539        Ok(Vec::new())
540    }
541
542    /// Deallocate GPU buffer
543    pub fn deallocate_buffer(&self, _handle: super::memory::BufferHandle) -> ComputeResult<()> {
544        // TODO: Implement GPU buffer deallocation
545        Ok(())
546    }
547}
548
549impl PerformanceTracker {
550    fn new() -> Self {
551        Self {
552            operation_counts: HashMap::new(),
553            execution_times: HashMap::new(),
554            backend_switches: HashMap::new(),
555            optimization_events: Vec::new(),
556        }
557    }
558
559    fn record_operation(&mut self, operation: &str, duration: f64, backend: BackendType) {
560        *self
561            .operation_counts
562            .entry(operation.to_string())
563            .or_insert(0) += 1;
564        self.execution_times
565            .entry(operation.to_string())
566            .or_default()
567            .push(duration);
568        *self.backend_switches.entry(backend).or_insert(0) += 1;
569    }
570
571    fn get_stats(&self) -> TrackerStats {
572        TrackerStats {
573            total_operations: self.operation_counts.values().sum(),
574            average_duration: self
575                .execution_times
576                .values()
577                .flat_map(|times| times.iter())
578                .sum::<f64>()
579                / self
580                    .execution_times
581                    .values()
582                    .map(|times| times.len())
583                    .sum::<usize>() as f64,
584            backend_distribution: self.backend_switches.clone(),
585            optimization_events: self.optimization_events.len(),
586        }
587    }
588}
589
590/// CPU activation function implementation
591fn apply_activation_cpu<T: Float>(x: T, function: ActivationFunction, steepness: T) -> T {
592    match function {
593        ActivationFunction::Linear => x * steepness,
594        ActivationFunction::Sigmoid => {
595            let exp_val = (-steepness * x).exp();
596            T::one() / (T::one() + exp_val)
597        }
598        ActivationFunction::ReLU => {
599            if x > T::zero() {
600                x
601            } else {
602                T::zero()
603            }
604        }
605        ActivationFunction::ReLULeaky => {
606            let alpha = T::from(0.01).unwrap_or(T::zero());
607            if x > T::zero() {
608                x
609            } else {
610                alpha * x
611            }
612        }
613        ActivationFunction::Tanh => (steepness * x).tanh(),
614        _ => x, // Fallback for other functions
615    }
616}
617
618/// Comprehensive performance statistics
619#[derive(Debug, Clone)]
620pub struct ComputePerformanceStats {
621    pub current_backend: BackendType,
622    pub gpu_available: bool,
623    pub cache_size: usize,
624    pub tracker_stats: Option<TrackerStats>,
625    pub gpu_stats: Option<PerformanceStats>,
626}
627
628/// Performance tracker statistics
629#[derive(Debug, Clone)]
630pub struct TrackerStats {
631    pub total_operations: u64,
632    pub average_duration: f64,
633    pub backend_distribution: HashMap<BackendType, u64>,
634    pub optimization_events: usize,
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640    use crate::NetworkBuilder;
641
642    #[tokio::test]
643    async fn test_compute_context_creation() {
644        let context = ComputeContext::<f32>::cpu_only();
645        assert!(!context.is_gpu_available());
646        assert_eq!(context.current_backend(), BackendType::Cpu);
647    }
648
649    #[tokio::test]
650    async fn test_backend_selection() {
651        let mut context = ComputeContext::<f32>::cpu_only();
652
653        // Small problems should prefer CPU/SIMD
654        let backend = context.select_backend(100);
655        assert!(matches!(backend, BackendType::Cpu | BackendType::Simd));
656
657        // Large problems would prefer GPU if available
658        let backend = context.select_backend(1000000);
659        // Since GPU is not available in test, should fallback to SIMD/CPU
660        assert!(matches!(backend, BackendType::Cpu | BackendType::Simd));
661    }
662
663    #[tokio::test]
664    async fn test_network_forward_pass() {
665        let mut context = ComputeContext::<f32>::cpu_only();
666
667        // Create a simple test network
668        let network = NetworkBuilder::<f32>::new()
669            .input_layer(2)
670            .hidden_layer(3)
671            .output_layer(1)
672            .build();
673
674        let inputs = vec![0.5f32, 0.7f32];
675
676        // Debug network structure
677        println!("Network structure:");
678        println!("  Layers: {}", network.layers.len());
679        for (i, layer) in network.layers.iter().enumerate() {
680            println!("  Layer {}: {} neurons", i, layer.neurons.len());
681
682            // Debug first neuron in each layer
683            if let Some(neuron) = layer.neurons.first() {
684                println!(
685                    "    First neuron has {} connections, is_bias: {}",
686                    neuron.connections.len(),
687                    neuron.is_bias
688                );
689            }
690        }
691
692        println!("Starting forward pass with {} inputs", inputs.len());
693        let result = context.compute_network_forward(&network, &inputs).await;
694
695        match &result {
696            Ok(outputs) => println!("Forward pass succeeded with {} outputs", outputs.len()),
697            Err(e) => println!("Forward pass failed: {e:?}"),
698        }
699
700        assert!(result.is_ok(), "Forward pass failed");
701
702        let outputs = result.unwrap();
703        assert_eq!(outputs.len(), 1, "Output should have 1 value");
704    }
705
706    #[tokio::test]
707    async fn test_performance_tracking() {
708        let context = ComputeContext::<f32>::cpu_only();
709        let stats = context.get_performance_stats();
710
711        assert_eq!(stats.current_backend, BackendType::Cpu);
712        assert!(!stats.gpu_available);
713        assert_eq!(stats.cache_size, 0);
714    }
715}