## Coverage Report

#### Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /home/noah/src/trueno/src/backends/gpu/mod.rs

    Line
    Count
    Source
    1
    //! GPU backend using wgpu (Vulkan/Metal/DX12/WebGPU)
    2
    //!
    3
    //! This backend provides GPU-accelerated compute for large-scale operations.
    4
    //! It uses wgpu for cross-platform GPU access and WGSL compute shaders.
    5
    //!
    6
    //! # Performance
    7
    //!
    8
    //! GPU backend is optimal for very large workloads (>100K elements for reductions,
    9
    //! >1000×1000 for matrix operations) where transfer overhead is amortized.
    10
    //!
    11
    //! Expected speedups vs SIMD:
    12
    //! - Matrix multiplication (large): 10-50x
    13
    //! - Reductions (large): 5-20x
    14
    //!
    15
    //! # Architecture
    16
    //!
    17
    //! - Device initialization is lazy (first GPU operation)
    18
    //! - Compute shaders written in WGSL
    19
    //! - Asynchronous execution with pollster for blocking
    20
    //! - Automatic fallback to CPU if GPU unavailable
    21
    //!
    22
    //! # Memory Hierarchy Abstractions
    23
    //!
    24
    //! - [`TensorView`] - Structured view into GPU memory with shape/stride metadata
    25
    //! - [`PartitionView`] - Tiling strategy for efficient GPU work distribution
    26
    //!
    27
    //! Based on cuda-tile-behavior.md Section 3.2.
    28
    29
    #[cfg(any(feature = "gpu", feature = "gpu-wasm"))]
    30
    mod batch;
    31
    32
    #[cfg(any(feature = "gpu", feature = "gpu-wasm"))]
    33
    mod device;
    34
    35
    #[cfg(any(feature = "gpu", feature = "gpu-wasm"))]
    36
    mod shaders;
    37
    38
    #[cfg(any(feature = "gpu", feature = "gpu-wasm"))]
    39
    pub mod runtime;
    40
    41
    // Memory hierarchy abstractions (always available, no GPU feature required)
    42
    mod partition_view;
    43
    mod tensor_view;
    44
    mod tiled_reduction;
    45
    46
    pub use partition_view::{PartitionView, TileInfo};
    47
    pub use tensor_view::{MemoryLayout, TensorView};
    48
    pub use tiled_reduction::{
    49
        tiled_max_2d, tiled_min_2d, tiled_reduce_2d, tiled_reduce_partial, tiled_sum_2d, MaxOp, MinOp,
    50
        ReduceOp, SumOp, TILE_SIZE,
    51
    };
    52
    53
    #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    54
    pub use batch::{BufferId, GpuCommandBatch};
    55
    56
    // Export GpuDevice for both native and WASM GPU features
    57
    #[cfg(any(feature = "gpu", feature = "gpu-wasm"))]
    58
    pub use device::GpuDevice;
    59
    60
    /// GPU backend for compute operations (native only, uses sync wrappers)
    61
    #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    62
    #[derive(Clone)]
    63
    pub struct GpuBackend {
    64
        device: Option<GpuDevice>,
    65
    }
    66
    67
    #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    68
    impl GpuBackend {
    69
        /// Create a new GPU backend
    70
    155
        pub fn new() -> Self {
    71
    155
            Self { device: None }
    72
    155
        }
    73
    74
        /// Initialize GPU device (lazy)
    75
    438
        fn ensure_device(&mut self) -> Result<&GpuDevice, String> {
    76
    438
            if self.device.is_none() {
    77
    47
                self.device = Some(GpuDevice::new()?0);
    78
    391
            }
    79
    438
            Ok(self.device.as_ref().expect("device initialized above"))
    80
    438
        }
    81
    82
        /// Check if GPU is available
    83
    155
        pub fn is_available() -> bool {
    84
    155
            GpuDevice::is_available()
    85
    155
        }
    86
    87
        /// Vector addition on GPU: c = a + b
    88
        ///
    89
        /// # Arguments
    90
        ///
    91
        /// * `a` - Vector a
    92
        /// * `b` - Vector b
    93
        ///
    94
        /// # Returns
    95
        ///
    96
        /// Vector c (element-wise sum)
    97
    0
        pub fn vec_add(&mut self, a: &[f32], b: &[f32]) -> Result<Vec<f32>, String> {
    98
    0
            if a.len() != b.len() {
    99
    0
                return Err(format!(
    100
    0
                    "Vector length mismatch: {} != {}",
    101
    0
                    a.len(),
    102
    0
                    b.len()
    103
    0
                ));
    104
    0
            }
    105
    106
            // wgpu doesn't allow zero-sized buffers
    107
    0
            if a.is_empty() {
    108
    0
                return Err("Cannot perform GPU operation on empty vectors".to_string());
    109
    0
            }
    110
    111
    0
            let device = self.ensure_device()?;
    112
    113
            // Create output buffer
    114
    0
            let mut result = vec![0.0f32; a.len()];
    115
    116
            // Execute GPU compute
    117
    0
            device.vec_add(a, b, &mut result)?;
    118
    119
    0
            Ok(result)
    120
    0
        }
    121
    122
        /// Dot product on GPU: result = sum(a[i] * b[i])
    123
        ///
    124
        /// # Arguments
    125
        ///
    126
        /// * `a` - Vector a
    127
        /// * `b` - Vector b
    128
        ///
    129
        /// # Returns
    130
        ///
    131
        /// Scalar dot product result
    132
    0
        pub fn dot(&mut self, a: &[f32], b: &[f32]) -> Result<f32, String> {
    133
    0
            if a.len() != b.len() {
    134
    0
                return Err(format!(
    135
    0
                    "Vector length mismatch: {} != {}",
    136
    0
                    a.len(),
    137
    0
                    b.len()
    138
    0
                ));
    139
    0
            }
    140
    141
    0
            let device = self.ensure_device()?;
    142
    143
            // Execute GPU compute
    144
    0
            device.dot(a, b)
    145
    0
        }
    146
    147
        /// ReLU activation on GPU: result[i] = max(0, input[i])
    148
        ///
    149
        /// # Arguments
    150
        ///
    151
        /// * `input` - Input vector
    152
        ///
    153
        /// # Returns
    154
        ///
    155
        /// Vector with ReLU applied element-wise
    156
    0
        pub fn relu(&mut self, input: &[f32]) -> Result<Vec<f32>, String> {
    157
    0
            let device = self.ensure_device()?;
    158
    159
            // Create output buffer
    160
    0
            let mut result = vec![0.0f32; input.len()];
    161
    162
            // Execute GPU compute
    163
    0
            device.relu(input, &mut result)?;
    164
    165
    0
            Ok(result)
    166
    0
        }
    167
    168
        /// Leaky ReLU activation on GPU: result[i] = max(negative_slope * input[i], input[i])
    169
        ///
    170
        /// # Arguments
    171
        ///
    172
        /// * `input` - Input vector
    173
        /// * `negative_slope` - Slope for negative values (typically 0.01)
    174
        ///
    175
        /// # Returns
    176
        ///
    177
        /// Vector with leaky ReLU applied element-wise
    178
    0
        pub fn leaky_relu(&mut self, input: &[f32], negative_slope: f32) -> Result<Vec<f32>, String> {
    179
    0
            let device = self.ensure_device()?;
    180
    181
            // Create output buffer
    182
    0
            let mut result = vec![0.0f32; input.len()];
    183
    184
            // Execute GPU compute
    185
    0
            device.leaky_relu(input, &mut result, negative_slope)?;
    186
    187
    0
            Ok(result)
    188
    0
        }
    189
    190
        /// ELU activation on GPU: result[i] = x if x > 0, else alpha * (exp(x) - 1)
    191
        ///
    192
        /// # Arguments
    193
        ///
    194
        /// * `input` - Input vector
    195
        /// * `alpha` - Scaling factor for negative values (typically 1.0)
    196
        ///
    197
        /// # Returns
    198
        ///
    199
        /// Vector with ELU applied element-wise
    200
    0
        pub fn elu(&mut self, input: &[f32], alpha: f32) -> Result<Vec<f32>, String> {
    201
    0
            let device = self.ensure_device()?;
    202
    203
            // Create output buffer
    204
    0
            let mut result = vec![0.0f32; input.len()];
    205
    206
            // Execute GPU compute
    207
    0
            device.elu(input, &mut result, alpha)?;
    208
    209
    0
            Ok(result)
    210
    0
        }
    211
    212
        /// Clip (clamp) operation on GPU: result[i] = clamp(input[i], min_val, max_val)
    213
        ///
    214
        /// # Arguments
    215
        ///
    216
        /// * `input` - Input vector
    217
        /// * `min_val` - Minimum value
    218
        /// * `max_val` - Maximum value
    219
        ///
    220
        /// # Returns
    221
        ///
    222
        /// Vector with clip applied element-wise
    223
    0
        pub fn clip(&mut self, input: &[f32], min_val: f32, max_val: f32) -> Result<Vec<f32>, String> {
    224
    0
            let device = self.ensure_device()?;
    225
    226
            // Create output buffer
    227
    0
            let mut result = vec![0.0f32; input.len()];
    228
    229
            // Execute GPU compute
    230
    0
            device.clip(input, &mut result, min_val, max_val)?;
    231
    232
    0
            Ok(result)
    233
    0
        }
    234
    235
        /// Sigmoid activation on GPU: result[i] = 1 / (1 + exp(-input[i]))
    236
        ///
    237
        /// # Arguments
    238
        ///
    239
        /// * `input` - Input vector
    240
        ///
    241
        /// # Returns
    242
        ///
    243
        /// Vector with sigmoid applied element-wise
    244
    0
        pub fn sigmoid(&mut self, input: &[f32]) -> Result<Vec<f32>, String> {
    245
    0
            let device = self.ensure_device()?;
    246
    247
            // Create output buffer
    248
    0
            let mut result = vec![0.0f32; input.len()];
    249
    250
            // Execute GPU compute
    251
    0
            device.sigmoid(input, &mut result)?;
    252
    253
    0
            Ok(result)
    254
    0
        }
    255
    256
        /// Tanh activation on GPU: result[i] = tanh(input[i])
    257
        ///
    258
        /// # Arguments
    259
        ///
    260
        /// * `input` - Input vector
    261
        ///
    262
        /// # Returns
    263
        ///
    264
        /// Vector with tanh applied element-wise
    265
    0
        pub fn tanh(&mut self, input: &[f32]) -> Result<Vec<f32>, String> {
    266
    0
            let device = self.ensure_device()?;
    267
    268
            // Create output buffer
    269
    0
            let mut result = vec![0.0f32; input.len()];
    270
    271
            // Execute GPU compute
    272
    0
            device.tanh(input, &mut result)?;
    273
    274
    0
            Ok(result)
    275
    0
        }
    276
    277
        /// Swish activation on GPU: result[i] = input[i] / (1 + exp(-input[i]))
    278
        ///
    279
        /// # Arguments
    280
        ///
    281
        /// * `input` - Input vector
    282
        ///
    283
        /// # Returns
    284
        ///
    285
        /// Vector with swish applied element-wise
    286
    0
        pub fn swish(&mut self, input: &[f32]) -> Result<Vec<f32>, String> {
    287
    0
            let device = self.ensure_device()?;
    288
    289
            // Create output buffer
    290
    0
            let mut result = vec![0.0f32; input.len()];
    291
    292
            // Execute GPU compute
    293
    0
            device.swish(input, &mut result)?;
    294
    295
    0
            Ok(result)
    296
    0
        }
    297
    298
        /// GELU activation on GPU: result[i] = 0.5 * input[i] * (1 + tanh(...))
    299
        ///
    300
        /// # Arguments
    301
        ///
    302
        /// * `input` - Input vector
    303
        ///
    304
        /// # Returns
    305
        ///
    306
        /// Vector with GELU applied element-wise
    307
    0
        pub fn gelu(&mut self, input: &[f32]) -> Result<Vec<f32>, String> {
    308
    0
            let device = self.ensure_device()?;
    309
    310
            // Create output buffer
    311
    0
            let mut result = vec![0.0f32; input.len()];
    312
    313
            // Execute GPU compute
    314
    0
            device.gelu(input, &mut result)?;
    315
    316
    0
            Ok(result)
    317
    0
        }
    318
    319
        /// Softmax activation on GPU: result[i] = exp(input[i] - max) / sum(exp(input - max))
    320
        ///
    321
        /// Uses multi-pass reduction for numerical stability:
    322
        /// - Pass 1: Max reduction (parallel)
    323
        /// - Pass 2: Exp-subtract (element-wise)
    324
        /// - Pass 3: Sum reduction (parallel)
    325
        /// - Pass 4: Normalize (element-wise)
    326
        ///
    327
        /// # Arguments
    328
        ///
    329
        /// * `input` - Input vector
    330
        ///
    331
        /// # Returns
    332
        ///
    333
        /// Vector with softmax applied element-wise
    334
    0
        pub fn softmax(&mut self, input: &[f32]) -> Result<Vec<f32>, String> {
    335
    0
            let device = self.ensure_device()?;
    336
    337
            // Create output buffer
    338
    0
            let mut result = vec![0.0f32; input.len()];
    339
    340
            // Execute GPU compute
    341
    0
            device.softmax(input, &mut result)?;
    342
    343
    0
            Ok(result)
    344
    0
        }
    345
    346
        /// Log-softmax activation on GPU: result[i] = log(softmax(input)[i])
    347
        ///
    348
        /// Uses multi-pass reduction for numerical stability:
    349
        /// - Pass 1: Max reduction (parallel)
    350
        /// - Pass 2: Exp-subtract (element-wise)
    351
        /// - Pass 3: Sum reduction (parallel)
    352
        /// - Pass 4: Log-normalize (element-wise)
    353
        ///
    354
        /// # Arguments
    355
        ///
    356
        /// * `input` - Input vector
    357
        ///
    358
        /// # Returns
    359
        ///
    360
        /// Vector with log-softmax applied element-wise
    361
    0
        pub fn log_softmax(&mut self, input: &[f32]) -> Result<Vec<f32>, String> {
    362
    0
            let device = self.ensure_device()?;
    363
    364
            // Create output buffer
    365
    0
            let mut result = vec![0.0f32; input.len()];
    366
    367
            // Execute GPU compute
    368
    0
            device.log_softmax(input, &mut result)?;
    369
    370
    0
            Ok(result)
    371
    0
        }
    372
    373
        /// 2D Convolution on GPU: output = input ⊗ kernel
    374
        ///
    375
        /// # Arguments
    376
        ///
    377
        /// * `input` - Input matrix (flattened row-major)
    378
        /// * `kernel` - Convolution kernel (flattened row-major)
    379
        /// * `input_rows` - Number of rows in input
    380
        /// * `input_cols` - Number of columns in input
    381
        /// * `kernel_rows` - Number of rows in kernel
    382
        /// * `kernel_cols` - Number of columns in kernel
    383
        ///
    384
        /// # Returns
    385
        ///
    386
        /// Output matrix (flattened row-major, "valid" convolution)
    387
        /// - output_rows = input_rows - kernel_rows + 1
    388
        /// - output_cols = input_cols - kernel_cols + 1
    389
    0
        pub fn convolve2d(
    390
    0
            &mut self,
    391
    0
            input: &[f32],
    392
    0
            kernel: &[f32],
    393
    0
            input_rows: usize,
    394
    0
            input_cols: usize,
    395
    0
            kernel_rows: usize,
    396
    0
            kernel_cols: usize,
    397
    0
        ) -> Result<Vec<f32>, String> {
    398
    0
            let device = self.ensure_device()?;
    399
    400
            // Calculate output dimensions
    401
    0
            let output_rows = input_rows.saturating_sub(kernel_rows).saturating_add(1);
    402
    0
            let output_cols = input_cols.saturating_sub(kernel_cols).saturating_add(1);
    403
    404
            // Create output buffer
    405
    0
            let mut result = vec![0.0f32; output_rows * output_cols];
    406
    407
            // Execute GPU compute
    408
    0
            device.convolve2d(
    409
    0
                input,
    410
    0
                kernel,
    411
    0
                &mut result,
    412
    0
                input_rows,
    413
    0
                input_cols,
    414
    0
                kernel_rows,
    415
    0
                kernel_cols,
    416
    0
            )?;
    417
    418
    0
            Ok(result)
    419
    0
        }
    420
    421
        /// Matrix multiplication on GPU: C = A × B
    422
        ///
    423
        /// # Arguments
    424
        ///
    425
        /// * `a` - Matrix A (m×k) in row-major order
    426
        /// * `b` - Matrix B (k×n) in row-major order
    427
        /// * `m` - Rows of A and C
    428
        /// * `k` - Cols of A, rows of B
    429
        /// * `n` - Cols of B and C
    430
        ///
    431
        /// # Returns
    432
        ///
    433
        /// Matrix C (m×n) in row-major order
    434
    438
        pub fn matmul(
    435
    438
            &mut self,
    436
    438
            a: &[f32],
    437
    438
            b: &[f32],
    438
    438
            m: usize,
    439
    438
            k: usize,
    440
    438
            n: usize,
    441
    438
        ) -> Result<Vec<f32>, String> {
    442
    438
            let device = self.ensure_device()?0;
    443
    444
            // Create output buffer
    445
    438
            let mut result = vec![0.0f32; m * n];
    446
    447
            // Execute GPU compute
    448
    438
            device.matmul(a, b, &mut result, m, k, n)?0;
    449
    450
    438
            Ok(result)
    451
    438
        }
    452
    453
        /// Symmetric eigendecomposition on GPU
    454
        ///
    455
        /// Computes eigenvalues and eigenvectors using Jacobi algorithm with
    456
        /// GPU-accelerated Givens rotations.
    457
        ///
    458
        /// # Arguments
    459
        ///
    460
        /// * `matrix` - Symmetric matrix data (row-major, n×n)
    461
        /// * `n` - Matrix dimension
    462
        ///
    463
        /// # Returns
    464
        ///
    465
        /// Tuple of (eigenvalues, eigenvector_data) where eigenvector_data is row-major
    466
    0
        pub fn symmetric_eigen(
    467
    0
            &mut self,
    468
    0
            matrix: &[f32],
    469
    0
            n: usize,
    470
    0
        ) -> Result<(Vec<f32>, Vec<f32>), String> {
    471
    0
            let device = self.ensure_device()?;
    472
    0
            device.symmetric_eigen(matrix, n)
    473
    0
        }
    474
    475
        /// 2D Tiled Sum Reduction on GPU
    476
        ///
    477
        /// Uses 16×16 workgroups for efficient parallel reduction with
    478
        /// optimal memory coalescing.
    479
        ///
    480
        /// # Arguments
    481
        ///
    482
        /// * `data` - Input 2D data in row-major order
    483
        /// * `width` - Number of columns
    484
        /// * `height` - Number of rows
    485
        ///
    486
        /// # Returns
    487
        ///
    488
        /// Sum of all elements
    489
    0
        pub fn tiled_sum_2d_gpu(
    490
    0
            &mut self,
    491
    0
            data: &[f32],
    492
    0
            width: usize,
    493
    0
            height: usize,
    494
    0
        ) -> Result<f32, String> {
    495
    0
            let device = self.ensure_device()?;
    496
    0
            device.tiled_sum_2d(data, width, height)
    497
    0
        }
    498
    499
        /// 2D Tiled Max Reduction on GPU
    500
        ///
    501
        /// Uses 16×16 workgroups for efficient parallel max reduction.
    502
    0
        pub fn tiled_max_2d_gpu(
    503
    0
            &mut self,
    504
    0
            data: &[f32],
    505
    0
            width: usize,
    506
    0
            height: usize,
    507
    0
        ) -> Result<f32, String> {
    508
    0
            let device = self.ensure_device()?;
    509
    0
            device.tiled_max_2d(data, width, height)
    510
    0
        }
    511
    512
        /// 2D Tiled Min Reduction on GPU
    513
        ///
    514
        /// Uses 16×16 workgroups for efficient parallel min reduction.
    515
    0
        pub fn tiled_min_2d_gpu(
    516
    0
            &mut self,
    517
    0
            data: &[f32],
    518
    0
            width: usize,
    519
    0
            height: usize,
    520
    0
        ) -> Result<f32, String> {
    521
    0
            let device = self.ensure_device()?;
    522
    0
            device.tiled_min_2d(data, width, height)
    523
    0
        }
    524
    }
    525
    526
    #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
    527
    impl Default for GpuBackend {
    528
    0
        fn default() -> Self {
    529
    0
            Self::new()
    530
    0
        }
    531
    }
    532
    533
    // Stub implementation when GPU feature is disabled or on WASM
    534
    #[cfg(any(not(feature = "gpu"), target_arch = "wasm32"))]
    535
    #[derive(Clone)]
    536
    pub struct GpuBackend;
    537
    538
    #[cfg(any(not(feature = "gpu"), target_arch = "wasm32"))]
    539
    impl GpuBackend {
    540
        pub fn new() -> Self {
    541
            Self
    542
        }
    543
    544
        pub fn is_available() -> bool {
    545
            false
    546
        }
    547
    }
    548
    549
    #[cfg(any(not(feature = "gpu"), target_arch = "wasm32"))]
    550
    impl Default for GpuBackend {
    551
        fn default() -> Self {
    552
            Self
    553
        }
    554
    }
    555
    556
    // Tests for stub implementation (when GPU feature is NOT enabled)
    557
    #[cfg(test)]
    558
    #[cfg(not(feature = "gpu"))]
    559
    mod stub_tests {
    560
        use super::*;
    561
    562
        #[test]
    563
        fn test_gpu_backend_stub_new() {
    564
            let _backend = GpuBackend::new();
    565
        }
    566
    567
        #[test]
    568
        fn test_gpu_backend_stub_is_available() {
    569
            assert!(!GpuBackend::is_available());
    570
        }
    571
    572
        #[test]
    573
        fn test_gpu_backend_stub_default() {
    574
            let _backend = GpuBackend::default();
    575
        }
    576
    577
        #[test]
    578
        fn test_gpu_backend_stub_clone() {
    579
            let backend = GpuBackend::new();
    580
            let _cloned = backend.clone();
    581
        }
    582
    }
    583
    584
    // ===== GPU Tests =====
    585
    586
    #[cfg(test)]
    587
    #[cfg(feature = "gpu")]
    588
    mod tests {
    589
        use super::*;
    590
        use std::sync::OnceLock;
    591
    592
        /// Shared GPU backend for fast test execution (initialized once)
    593
        static SHARED_GPU: OnceLock<Option<GpuBackend>> = OnceLock::new();
    594
    595
        /// Get shared GPU backend (fast) or None if unavailable
    596
        fn get_shared_gpu() -> Option<GpuBackend> {
    597
            SHARED_GPU
    598
                .get_or_init(|| {
    599
                    if GpuBackend::is_available() {
    600
                        Some(GpuBackend::new())
    601
                    } else {
    602
                        None
    603
                    }
    604
                })
    605
                .clone()
    606
        }
    607
    608
        #[test]
    609
        fn test_gpu_vec_add_basic() {
    610
            let Some(mut gpu) = get_shared_gpu() else {
    611
                eprintln!("GPU not available, skipping test");
    612
                return;
    613
            };
    614
            let a = vec![1.0, 2.0, 3.0, 4.0];
    615
            let b = vec![5.0, 6.0, 7.0, 8.0];
    616
    617
            let result = gpu.vec_add(&a, &b);
    618
    619
            if let Ok(c) = result {
    620
                assert_eq!(c.len(), 4);
    621
                assert!((c[0] - 6.0).abs() < 1e-4);
    622
                assert!((c[1] - 8.0).abs() < 1e-4);
    623
                assert!((c[2] - 10.0).abs() < 1e-4);
    624
                assert!((c[3] - 12.0).abs() < 1e-4);
    625
            } else {
    626
                eprintln!("GPU vec_add failed: {:?}", result);
    627
            }
    628
        }
    629
    630
        #[test]
    631
        fn test_gpu_vec_add_large() {
    632
            let Some(mut gpu) = get_shared_gpu() else {
    633
                eprintln!("GPU not available, skipping test");
    634
                return;
    635
            };
    636
            let size = 10000;
    637
            let a: Vec<f32> = (0..size).map(|i| i as f32).collect();
    638
            let b: Vec<f32> = (0..size).map(|i| (i * 2) as f32).collect();
    639
    640
            let result = gpu.vec_add(&a, &b);
    641
    642
            if let Ok(c) = result {
    643
                assert_eq!(c.len(), size);
    644
                // Check first few elements
    645
                assert!((c[0] - 0.0).abs() < 1e-4); // 0 + 0
    646
                assert!((c[1] - 3.0).abs() < 1e-4); // 1 + 2
    647
                assert!((c[100] - 300.0).abs() < 1e-4); // 100 + 200
    648
            } else {
    649
                eprintln!("GPU vec_add large failed: {:?}", result);
    650
            }
    651
        }
    652
    653
        #[test]
    654
        fn test_gpu_vec_add_length_mismatch() {
    655
            let Some(mut gpu) = get_shared_gpu() else {
    656
                eprintln!("GPU not available, skipping test");
    657
                return;
    658
            };
    659
            let a = vec![1.0, 2.0, 3.0];
    660
            let b = vec![4.0, 5.0]; // Different length
    661
    662
            let result = gpu.vec_add(&a, &b);
    663
            assert!(result.is_err());
    664
        }
    665
    666
        #[test]
    667
        fn test_gpu_dot_basic() {
    668
            let Some(mut gpu) = get_shared_gpu() else {
    669
                eprintln!("GPU not available, skipping test");
    670
                return;
    671
            };
    672
            let a = vec![1.0, 2.0, 3.0, 4.0];
    673
            let b = vec![5.0, 6.0, 7.0, 8.0];
    674
    675
            let result = gpu.dot(&a, &b);
    676
    677
            // Expected: 1*5 + 2*6 + 3*7 + 4*8 = 5 + 12 + 21 + 32 = 70
    678
            if let Ok(dot_product) = result {
    679
                assert!((dot_product - 70.0).abs() < 1e-4);
    680
            } else {
    681
                eprintln!("GPU dot failed: {:?}", result);
    682
            }
    683
        }
    684
    685
        #[test]
    686
        fn test_gpu_dot_large() {
    687
            let Some(mut gpu) = get_shared_gpu() else {
    688
                eprintln!("GPU not available, skipping test");
    689
                return;
    690
            };
    691
            let size = 10000;
    692
            let a: Vec<f32> = (0..size).map(|i| i as f32).collect();
    693
            let b: Vec<f32> = (0..size).map(|_| 1.0).collect();
    694
    695
            let result = gpu.dot(&a, &b);
    696
    697
            // Expected: sum of 0 + 1 + 2 + ... + 9999 = 9999 * 10000 / 2 = 49995000
    698
            if let Ok(dot_product) = result {
    699
                let expected = (size * (size - 1) / 2) as f32;
    700
                assert!((dot_product - expected).abs() < 1.0); // Allow small floating point error
    701
            } else {
    702
                eprintln!("GPU dot large failed: {:?}", result);
    703
            }
    704
        }
    705
    706
        #[test]
    707
        fn test_gpu_dot_length_mismatch() {
    708
            let Some(mut gpu) = get_shared_gpu() else {
    709
                eprintln!("GPU not available, skipping test");
    710
                return;
    711
            };
    712
            let a = vec![1.0, 2.0, 3.0];
    713
            let b = vec![4.0, 5.0]; // Different length
    714
    715
            let result = gpu.dot(&a, &b);
    716
            assert!(result.is_err());
    717
        }
    718
    719
        #[test]
    720
        fn test_gpu_vec_add_matches_scalar() {
    721
            if !GpuBackend::is_available() {
    722
                eprintln!("GPU not available, skipping test");
    723
                return;
    724
            }
    725
    726
            use super::super::scalar::ScalarBackend;
    727
            use crate::backends::VectorBackend;
    728
    729
            let mut gpu = GpuBackend::new();
    730
            let a = vec![1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5];
    731
            let b = vec![8.5, 7.5, 6.5, 5.5, 4.5, 3.5, 2.5, 1.5];
    732
    733
            let gpu_result = gpu.vec_add(&a, &b);
    734
    735
            let mut scalar_result = vec![0.0; 8];
    736
            // SAFETY: Test code calling backend trait methods marked unsafe
    737
            unsafe {
    738
                ScalarBackend::add(&a, &b, &mut scalar_result);
    739
            }
    740
    741
            if let Ok(gpu_r) = gpu_result {
    742
                for (g, s) in gpu_r.iter().zip(scalar_result.iter()) {
    743
                    assert!(
    744
                        (g - s).abs() < 1e-4,
    745
                        "GPU vs Scalar mismatch: gpu={}, scalar={}",
    746
                        g,
    747
                        s
    748
                    );
    749
                }
    750
            } else {
    751
                eprintln!("GPU vec_add failed: {:?}", gpu_result);
    752
            }
    753
        }
    754
    755
        #[test]
    756
        fn test_gpu_dot_matches_scalar() {
    757
            if !GpuBackend::is_available() {
    758
                eprintln!("GPU not available, skipping test");
    759
                return;
    760
            }
    761
    762
            use super::super::scalar::ScalarBackend;
    763
            use crate::backends::VectorBackend;
    764
    765
            let mut gpu = GpuBackend::new();
    766
            let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
    767
            let b = vec![8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0];
    768
    769
            let gpu_result = gpu.dot(&a, &b);
    770
            // SAFETY: Test code calling backend trait methods marked unsafe
    771
            let scalar_result = unsafe { ScalarBackend::dot(&a, &b) };
    772
    773
            if let Ok(gpu_r) = gpu_result {
    774
                assert!(
    775
                    (gpu_r - scalar_result).abs() < 1e-4,
    776
                    "GPU vs Scalar dot mismatch: gpu={}, scalar={}",
    777
                    gpu_r,
    778
                    scalar_result
    779
                );
    780
            } else {
    781
                eprintln!("GPU dot failed: {:?}", gpu_result);
    782
            }
    783
        }
    784
    785
        #[test]
    786
        fn test_gpu_vec_add_empty() {
    787
            let Some(mut gpu) = get_shared_gpu() else {
    788
                eprintln!("GPU not available, skipping test");
    789
                return;
    790
            };
    791
            let a: Vec<f32> = vec![];
    792
            let b: Vec<f32> = vec![];
    793
    794
            let result = gpu.vec_add(&a, &b);
    795
    796
            // GPU backend returns error for empty vectors (wgpu doesn't allow zero-sized buffers)
    797
            assert!(
    798
                result.is_err(),
    799
                "Expected error for empty vectors, got: {:?}",
    800
                result
    801
            );
    802
        }
    803
    804
        #[test]
    805
        fn test_gpu_relu_matches_scalar() {
    806
            if !GpuBackend::is_available() {
    807
                eprintln!("GPU not available, skipping test");
    808
                return;
    809
            }
    810
    811
            use super::super::scalar::ScalarBackend;
    812
            use crate::backends::VectorBackend;
    813
    814
            let mut gpu = GpuBackend::new();
    815
            let input = vec![-3.0, -1.0, 0.0, 1.0, 3.0, -2.5, 2.5, 0.5];
    816
    817
            let gpu_result = gpu.relu(&input);
    818
            let mut scalar_result = vec![0.0; input.len()];
    819
            // SAFETY: Test code calling backend trait methods marked unsafe
    820
            unsafe {
    821
                ScalarBackend::relu(&input, &mut scalar_result);
    822
            }
    823
    824
            if let Ok(gpu_r) = gpu_result {
    825
                for (g, s) in gpu_r.iter().zip(scalar_result.iter()) {
    826
                    assert!(
    827
                        (g - s).abs() < 1e-4,
    828
                        "GPU vs Scalar relu mismatch: gpu={}, scalar={}",
    829
                        g,
    830
                        s
    831
                    );
    832
                }
    833
            } else {
    834
                eprintln!("GPU relu failed: {:?}", gpu_result);
    835
            }
    836
        }
    837
    838
        #[test]
    839
        fn test_gpu_sigmoid_matches_scalar() {
    840
            if !GpuBackend::is_available() {
    841
                eprintln!("GPU not available, skipping test");
    842
                return;
    843
            }
    844
    845
            use super::super::scalar::ScalarBackend;
    846
            use crate::backends::VectorBackend;
    847
    848
            let mut gpu = GpuBackend::new();
    849
            let input = vec![-3.0, -1.0, 0.0, 1.0, 3.0, -2.5, 2.5, 0.5];
    850
    851
            let gpu_result = gpu.sigmoid(&input);
    852
            let mut scalar_result = vec![0.0; input.len()];
    853
            // SAFETY: Test code calling backend trait methods marked unsafe
    854
            unsafe {
    855
                ScalarBackend::sigmoid(&input, &mut scalar_result);
    856
            }
    857
    858
            if let Ok(gpu_r) = gpu_result {
    859
                for (g, s) in gpu_r.iter().zip(scalar_result.iter()) {
    860
                    assert!(
    861
                        (g - s).abs() < 1e-3,
    862
                        "GPU vs Scalar sigmoid mismatch: gpu={}, scalar={}",
    863
                        g,
    864
                        s
    865
                    );
    866
                }
    867
            } else {
    868
                eprintln!("GPU sigmoid failed: {:?}", gpu_result);
    869
            }
    870
        }
    871
    872
        #[test]
    873
        fn test_gpu_gelu_matches_scalar() {
    874
            if !GpuBackend::is_available() {
    875
                eprintln!("GPU not available, skipping test");
    876
                return;
    877
            }
    878
    879
            use super::super::scalar::ScalarBackend;
    880
            use crate::backends::VectorBackend;
    881
    882
            let mut gpu = GpuBackend::new();
    883
            let input = vec![-2.0, -1.0, 0.0, 1.0, 2.0, -0.5, 0.5, 1.5];
    884
    885
            let gpu_result = gpu.gelu(&input);
    886
            let mut scalar_result = vec![0.0; input.len()];
    887
            // SAFETY: Test code calling backend trait methods marked unsafe
    888
            unsafe {
    889
                ScalarBackend::gelu(&input, &mut scalar_result);
    890
            }
    891
    892
            if let Ok(gpu_r) = gpu_result {
    893
                for (g, s) in gpu_r.iter().zip(scalar_result.iter()) {
    894
                    assert!(
    895
                        (g - s).abs() < 1e-2,
    896
                        "GPU vs Scalar gelu mismatch: gpu={}, scalar={}",
    897
                        g,
    898
                        s
    899
                    );
    900
                }
    901
            } else {
    902
                eprintln!("GPU gelu failed: {:?}", gpu_result);
    903
            }
    904
        }
    905
    906
        #[test]
    907
        fn test_gpu_swish_matches_scalar() {
    908
            if !GpuBackend::is_available() {
    909
                eprintln!("GPU not available, skipping test");
    910
                return;
    911
            }
    912
    913
            use super::super::scalar::ScalarBackend;
    914
            use crate::backends::VectorBackend;
    915
    916
            let mut gpu = GpuBackend::new();
    917
            let input = vec![-3.0, -1.0, 0.0, 1.0, 3.0, -2.5, 2.5, 0.5];
    918
    919
            let gpu_result = gpu.swish(&input);
    920
            let mut scalar_result = vec![0.0; input.len()];
    921
            // SAFETY: Test code calling backend trait methods marked unsafe
    922
            unsafe {
    923
                ScalarBackend::swish(&input, &mut scalar_result);
    924
            }
    925
    926
            if let Ok(gpu_r) = gpu_result {
    927
                for (g, s) in gpu_r.iter().zip(scalar_result.iter()) {
    928
                    assert!(
    929
                        (g - s).abs() < 1e-3,
    930
                        "GPU vs Scalar swish mismatch: gpu={}, scalar={}",
    931
                        g,
    932
                        s
    933
                    );
    934
                }
    935
            } else {
    936
                eprintln!("GPU swish failed: {:?}", gpu_result);
    937
            }
    938
        }
    939
    940
        #[test]
    941
        fn test_gpu_clip_matches_scalar() {
    942
            if !GpuBackend::is_available() {
    943
                eprintln!("GPU not available, skipping test");
    944
                return;
    945
            }
    946
    947
            use super::super::scalar::ScalarBackend;
    948
            use crate::backends::VectorBackend;
    949
    950
            let mut gpu = GpuBackend::new();
    951
            let input = vec![1.0, 5.0, 10.0, 15.0, -3.0, 20.0, 7.5, 0.0];
    952
            let min_val = 3.0;
    953
            let max_val = 12.0;
    954
    955
            let gpu_result = gpu.clip(&input, min_val, max_val);
    956
            let mut scalar_result = vec![0.0; input.len()];
    957
            // SAFETY: Test code calling backend trait methods marked unsafe
    958
            unsafe {
    959
                ScalarBackend::clamp(&input, min_val, max_val, &mut scalar_result);
    960
            }
    961
    962
            if let Ok(gpu_r) = gpu_result {
    963
                for (g, s) in gpu_r.iter().zip(scalar_result.iter()) {
    964
                    assert!(
    965
                        (g - s).abs() < 1e-4,
    966
                        "GPU vs Scalar clip mismatch: gpu={}, scalar={}",
    967
                        g,
    968
                        s
    969
                    );
    970
                }
    971
            } else {
    972
                eprintln!("GPU clip failed: {:?}", gpu_result);
    973
            }
    974
        }
    975
    976
        #[test]
    977
        fn test_gpu_leaky_relu_basic() {
    978
            let Some(mut gpu) = get_shared_gpu() else {
    979
                eprintln!("GPU not available, skipping test");
    980
                return;
    981
            };
    982
            let input = vec![-3.0, -1.0, 0.0, 1.0, 3.0];
    983
            let negative_slope = 0.01;
    984
    985
            let result = gpu.leaky_relu(&input, negative_slope);
    986
    987
            if let Ok(output) = result {
    988
                // Expected: max(negative_slope * x, x)
    989
                let expected = [-0.03, -0.01, 0.0, 1.0, 3.0];
    990
                for (r, e) in output.iter().zip(expected.iter()) {
    991
                    assert!(
    992
                        (r - e).abs() < 1e-4,
    993
                        "Leaky ReLU mismatch: got={}, expected={}",
    994
                        r,
    995
                        e
    996
                    );
    997
                }
    998
            } else {
    999
                eprintln!("GPU leaky_relu failed: {:?}", result);
    1000
            }
    1001
        }
    1002
    1003
        #[test]
    1004
        fn test_gpu_elu_basic() {
    1005
            let Some(mut gpu) = get_shared_gpu() else {
    1006
                eprintln!("GPU not available, skipping test");
    1007
                return;
    1008
            };
    1009
            let input = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
    1010
            let alpha = 1.0;
    1011
    1012
            let result = gpu.elu(&input, alpha);
    1013
    1014
            if let Ok(output) = result {
    1015
                // Expected: x if x > 0, else alpha * (exp(x) - 1)
    1016
                for (i, (r, &x)) in output.iter().zip(input.iter()).enumerate() {
    1017
                    let expected = if x > 0.0 { x } else { alpha * (x.exp() - 1.0) };
    1018
                    assert!(
    1019
                        (r - expected).abs() < 1e-3,
    1020
                        "ELU mismatch at {}: got={}, expected={}",
    1021
                        i,
    1022
                        r,
    1023
                        expected
    1024
                    );
    1025
                }
    1026
            } else {
    1027
                eprintln!("GPU elu failed: {:?}", result);
    1028
            }
    1029
        }
    1030
    1031
        #[test]
    1032
        fn test_gpu_tanh_basic() {
    1033
            let Some(mut gpu) = get_shared_gpu() else {
    1034
                eprintln!("GPU not available, skipping test");
    1035
                return;
    1036
            };
    1037
            let input = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
    1038
    1039
            let result = gpu.tanh(&input);
    1040
    1041
            if let Ok(output) = result {
    1042
                for (r, &x) in output.iter().zip(input.iter()) {
    1043
                    let expected = x.tanh();
    1044
                    assert!(
    1045
                        (r - expected).abs() < 1e-4,
    1046
                        "Tanh mismatch: got={}, expected={}",
    1047
                        r,
    1048
                        expected
    1049
                    );
    1050
                }
    1051
            } else {
    1052
                eprintln!("GPU tanh failed: {:?}", result);
    1053
            }
    1054
        }
    1055
    1056
        #[test]
    1057
        fn test_gpu_tanh_not_hardcoded() {
    1058
            // EXTREME TDD: Kill mutant that replaces return with Ok(vec![-1.0])
    1059
            let Some(mut gpu) = get_shared_gpu() else {
    1060
                eprintln!("GPU not available, skipping test");
    1061
                return;
    1062
            };
    1063
            let input = vec![1.0, 2.0, 3.0];
    1064
    1065
            let result = gpu.tanh(&input).expect("GPU tanh should succeed");
    1066
    1067
            // Kill mutant: verify result is NOT all -1.0 values
    1068
            assert_ne!(
    1069
                result,
    1070
                vec![-1.0, -1.0, -1.0],
    1071
                "GPU tanh returned hardcoded -1.0 values (mutant not killed)"
    1072
            );
    1073
    1074
            // Verify correct computation
    1075
            for (i, &x) in input.iter().enumerate() {
    1076
                let expected = x.tanh();
    1077
                assert!(
    1078
                    (result[i] - expected).abs() < 1e-4,
    1079
                    "tanh({}) = {} (expected {})",
    1080
                    x,
    1081
                    result[i],
    1082
                    expected
    1083
                );
    1084
            }
    1085
        }
    1086
    1087
        #[test]
    1088
        fn test_gpu_softmax_basic() {
    1089
            let Some(mut gpu) = get_shared_gpu() else {
    1090
                eprintln!("GPU not available, skipping test");
    1091
                return;
    1092
            };
    1093
            let input = vec![1.0, 2.0, 3.0, 4.0];
    1094
    1095
            let result = gpu.softmax(&input);
    1096
    1097
            if let Ok(output) = result {
    1098
                // Softmax should sum to 1
    1099
                let sum: f32 = output.iter().sum();
    1100
                assert!(
    1101
                    (sum - 1.0).abs() < 1e-3,
    1102
                    "Softmax sum should be 1, got {}",
    1103
                    sum
    1104
                );
    1105
    1106
                // All values should be positive
    1107
                for &v in &output {
    1108
                    assert!(v > 0.0, "Softmax values should be positive");
    1109
                }
    1110
    1111
                // Later values should be larger (input is increasing)
    1112
                for i in 1..output.len() {
    1113
                    assert!(
    1114
                        output[i] > output[i - 1],
    1115
                        "Softmax should preserve order for increasing input"
    1116
                    );
    1117
                }
    1118
            } else {
    1119
                eprintln!("GPU softmax failed: {:?}", result);
    1120
            }
    1121
        }
    1122
    1123
        #[test]
    1124
        fn test_gpu_log_softmax_basic() {
    1125
            let Some(mut gpu) = get_shared_gpu() else {
    1126
                eprintln!("GPU not available, skipping test");
    1127
                return;
    1128
            };
    1129
            let input = vec![1.0, 2.0, 3.0, 4.0];
    1130
    1131
            let result = gpu.log_softmax(&input);
    1132
    1133
            if let Ok(output) = result {
    1134
                // log_softmax values should all be negative (log of probability < 1)
    1135
                for &v in &output {
    1136
                    assert!(v <= 0.0, "Log softmax values should be <= 0, got {}", v);
    1137
                }
    1138
    1139
                // exp(log_softmax) should sum to 1
    1140
                let exp_sum: f32 = output.iter().map(|x| x.exp()).sum();
    1141
                assert!(
    1142
                    (exp_sum - 1.0).abs() < 1e-3,
    1143
                    "exp(log_softmax) should sum to 1, got {}",
    1144
                    exp_sum
    1145
                );
    1146
            } else {
    1147
                eprintln!("GPU log_softmax failed: {:?}", result);
    1148
            }
    1149
        }
    1150
    1151
        #[test]
    1152
        fn test_gpu_matmul_basic() {
    1153
            let Some(mut gpu) = get_shared_gpu() else {
    1154
                eprintln!("GPU not available, skipping test");
    1155
                return;
    1156
            };
    1157
    1158
            // Simple 2x2 matrix multiplication
    1159
            // A = [[1, 2], [3, 4]]
    1160
            // B = [[5, 6], [7, 8]]
    1161
            // C = A * B = [[19, 22], [43, 50]]
    1162
            let a = vec![1.0, 2.0, 3.0, 4.0];
    1163
            let b = vec![5.0, 6.0, 7.0, 8.0];
    1164
    1165
            let res = gpu.matmul(&a, &b, 2, 2, 2);
    1166
    1167
            if let Ok(result) = res {
    1168
                assert!(
    1169
                    (result[0] - 19.0).abs() < 1e-3,
    1170
                    "Expected 19.0, got {}",
    1171
                    result[0]
    1172
                );
    1173
                assert!(
    1174
                    (result[1] - 22.0).abs() < 1e-3,
    1175
                    "Expected 22.0, got {}",
    1176
                    result[1]
    1177
                );
    1178
                assert!(
    1179
                    (result[2] - 43.0).abs() < 1e-3,
    1180
                    "Expected 43.0, got {}",
    1181
                    result[2]
    1182
                );
    1183
                assert!(
    1184
                    (result[3] - 50.0).abs() < 1e-3,
    1185
                    "Expected 50.0, got {}",
    1186
                    result[3]
    1187
                );
    1188
            } else {
    1189
                eprintln!("GPU matmul failed: {:?}", res);
    1190
            }
    1191
        }
    1192
    1193
        #[test]
    1194
        fn test_gpu_matmul_identity() {
    1195
            let Some(mut gpu) = get_shared_gpu() else {
    1196
                eprintln!("GPU not available, skipping test");
    1197
                return;
    1198
            };
    1199
    1200
            // Multiply by identity matrix
    1201
            // A = [[1, 2], [3, 4]]
    1202
            // I = [[1, 0], [0, 1]]
    1203
            // A * I = A
    1204
            let a = vec![1.0, 2.0, 3.0, 4.0];
    1205
            let identity = vec![1.0, 0.0, 0.0, 1.0];
    1206
    1207
            let res = gpu.matmul(&a, &identity, 2, 2, 2);
    1208
    1209
            if let Ok(result) = res {
    1210
                for i in 0..4 {
    1211
                    assert!(
    1212
                        (result[i] - a[i]).abs() < 1e-3,
    1213
                        "Expected {}, got {}",
    1214
                        a[i],
    1215
                        result[i]
    1216
                    );
    1217
                }
    1218
            } else {
    1219
                eprintln!("GPU matmul identity failed: {:?}", res);
    1220
            }
    1221
        }
    1222
    1223
        #[test]
    1224
        fn test_gpu_matmul_non_square() {
    1225
            let Some(mut gpu) = get_shared_gpu() else {
    1226
                eprintln!("GPU not available, skipping test");
    1227
                return;
    1228
            };
    1229
    1230
            // 2x3 matrix * 3x2 matrix = 2x2 matrix
    1231
            // A = [[1, 2, 3], [4, 5, 6]]
    1232
            // B = [[7, 8], [9, 10], [11, 12]]
    1233
            // C = [[58, 64], [139, 154]]
    1234
            let a = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
    1235
            let b = vec![7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
    1236
    1237
            let res = gpu.matmul(&a, &b, 2, 3, 2);
    1238
    1239
            if let Ok(result) = res {
    1240
                assert!(
    1241
                    (result[0] - 58.0).abs() < 1e-3,
    1242
                    "Expected 58.0, got {}",
    1243
                    result[0]
    1244
                );
    1245
                assert!(
    1246
                    (result[1] - 64.0).abs() < 1e-3,
    1247
                    "Expected 64.0, got {}",
    1248
                    result[1]
    1249
                );
    1250
                assert!(
    1251
                    (result[2] - 139.0).abs() < 1e-3,
    1252
                    "Expected 139.0, got {}",
    1253
                    result[2]
    1254
                );
    1255
                assert!(
    1256
                    (result[3] - 154.0).abs() < 1e-3,
    1257
                    "Expected 154.0, got {}",
    1258
                    result[3]
    1259
                );
    1260
            } else {
    1261
                eprintln!("GPU matmul non-square failed: {:?}", res);
    1262
            }
    1263
        }
    1264
    1265
        #[test]
    1266
        fn test_gpu_convolve2d_basic() {
    1267
            let Some(mut gpu) = get_shared_gpu() else {
    1268
                eprintln!("GPU not available, skipping test");
    1269
                return;
    1270
            };
    1271
    1272
            // 3x3 input, 2x2 kernel -> 2x2 output
    1273
            let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
    1274
            let kernel = vec![1.0, 0.0, 0.0, 1.0];
    1275
    1276
            let res = gpu.convolve2d(&input, &kernel, 3, 3, 2, 2);
    1277
    1278
            if let Ok(result) = res {
    1279
                // For kernel [[1, 0], [0, 1]], each output is sum of diagonal elements
    1280
                // Output[0,0] = input[0,0]*1 + input[1,1]*1 = 1 + 5 = 6
    1281
                assert!(
    1282
                    (result[0] - 6.0).abs() < 1e-3,
    1283
                    "Expected 6.0, got {}",
    1284
                    result[0]
    1285
                );
    1286
            } else {
    1287
                eprintln!("GPU convolve2d basic failed: {:?}", res);
    1288
            }
    1289
        }
    1290
    1291
        #[test]
    1292
        fn test_gpu_convolve2d_identity() {
    1293
            let Some(mut gpu) = get_shared_gpu() else {
    1294
                eprintln!("GPU not available, skipping test");
    1295
                return;
    1296
            };
    1297
    1298
            // 3x3 input with center-only kernel should extract center values
    1299
            let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
    1300
            // 3x3 kernel with center = 1, rest = 0
    1301
            let kernel = vec![0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0];
    1302
    1303
            let res = gpu.convolve2d(&input, &kernel, 3, 3, 3, 3);
    1304
    1305
            if let Ok(result) = res {
    1306
                // Should extract center value
    1307
                assert!(
    1308
                    (result[0] - 5.0).abs() < 1e-3,
    1309
                    "Expected 5.0, got {}",
    1310
                    result[0]
    1311
                );
    1312
            } else {
    1313
                eprintln!("GPU convolve2d identity failed: {:?}", res);
    1314
            }
    1315
        }
    1316
    1317
        #[test]
    1318
        fn test_gpu_convolve2d_averaging() {
    1319
            let Some(mut gpu) = get_shared_gpu() else {
    1320
                eprintln!("GPU not available, skipping test");
    1321
                return;
    1322
            };
    1323
    1324
            // 4x4 input with 2x2 averaging kernel
    1325
            let input = vec![
    1326
                1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
    1327
            ];
    1328
            // 2x2 averaging kernel
    1329
            let kernel = vec![0.25, 0.25, 0.25, 0.25];
    1330
    1331
            let res = gpu.convolve2d(&input, &kernel, 4, 4, 2, 2);
    1332
    1333
            if let Ok(result) = res {
    1334
                // First output: average of top-left 2x2 = (1+2+5+6)/4 = 3.5
    1335
                assert!(
    1336
                    (result[0] - 3.5).abs() < 1e-3,
    1337
                    "Expected 3.5, got {}",
    1338
                    result[0]
    1339
                );
    1340
            } else {
    1341
                eprintln!("GPU convolve2d averaging failed: {:?}", res);
    1342
            }
    1343
        }
    1344
    1345
        #[test]
    1346
        fn test_gpu_tiled_sum_matches_cpu() {
    1347
            let Some(mut gpu) = get_shared_gpu() else {
    1348
                eprintln!("GPU not available, skipping test");
    1349
                return;
    1350
            };
    1351
    1352
            // Test various sizes
    1353
            let test_cases = [
    1354
                (4, 4),   // Small (single partial tile)
    1355
                (16, 16), // Exact tile
    1356
                (32, 32), // Multiple tiles (2x2)
    1357
                (20, 20), // Non-aligned
    1358
                (100, 5), // Wide
    1359
                (5, 100), // Tall
    1360
            ];
    1361
    1362
            for (width, height) in test_cases {
    1363
                let data: Vec<f32> = (1..=(width * height) as i32).map(|x| x as f32).collect();
    1364
                let cpu_result = tiled_sum_2d(&data, width, height);
    1365
    1366
                if let Ok(gpu_result) = gpu.tiled_sum_2d_gpu(&data, width, height) {
    1367
                    let rel_err = (gpu_result - cpu_result).abs() / cpu_result.abs().max(1.0);
    1368
                    assert!(
    1369
                        rel_err < 1e-4,
    1370
                        "GPU vs CPU tiled_sum mismatch for {}x{}: gpu={}, cpu={}, rel_err={}",
    1371
                        width,
    1372
                        height,
    1373
                        gpu_result,
    1374
                        cpu_result,
    1375
                        rel_err
    1376
                    );
    1377
                } else {
    1378
                    eprintln!("GPU tiled_sum_2d failed for {}x{}", width, height);
    1379
                }
    1380
            }
    1381
        }
    1382
    1383
        #[test]
    1384
        fn test_gpu_tiled_max_matches_cpu() {
    1385
            let Some(mut gpu) = get_shared_gpu() else {
    1386
                eprintln!("GPU not available, skipping test");
    1387
                return;
    1388
            };
    1389
    1390
            // Test data with varying values
    1391
            let data: Vec<f32> = (1..=256).map(|x| x as f32).collect();
    1392
            let width = 16;
    1393
            let height = 16;
    1394
    1395
            let cpu_result = tiled_max_2d(&data, width, height);
    1396
    1397
            if let Ok(gpu_result) = gpu.tiled_max_2d_gpu(&data, width, height) {
    1398
                assert!(
    1399
                    (gpu_result - cpu_result).abs() < 1e-5,
    1400
                    "GPU vs CPU tiled_max mismatch: gpu={}, cpu={}",
    1401
                    gpu_result,
    1402
                    cpu_result
    1403
                );
    1404
            } else {
    1405
                eprintln!("GPU tiled_max_2d failed");
    1406
            }
    1407
        }
    1408
    1409
        #[test]
    1410
        fn test_gpu_tiled_min_matches_cpu() {
    1411
            let Some(mut gpu) = get_shared_gpu() else {
    1412
                eprintln!("GPU not available, skipping test");
    1413
                return;
    1414
            };
    1415
    1416
            // Test data with varying values including negatives
    1417
            let data: Vec<f32> = (-128..=127).map(|x| x as f32).collect();
    1418
            let width = 16;
    1419
            let height = 16;
    1420
    1421
            let cpu_result = tiled_min_2d(&data, width, height);
    1422
    1423
            if let Ok(gpu_result) = gpu.tiled_min_2d_gpu(&data, width, height) {
    1424
                assert!(
    1425
                    (gpu_result - cpu_result).abs() < 1e-5,
    1426
                    "GPU vs CPU tiled_min mismatch: gpu={}, cpu={}",
    1427
                    gpu_result,
    1428
                    cpu_result
    1429
                );
    1430
            } else {
    1431
                eprintln!("GPU tiled_min_2d failed");
    1432
            }
    1433
        }
    1434
    1435
        #[test]
    1436
        fn test_gpu_tiled_sum_large_matrix() {
    1437
            let Some(mut gpu) = get_shared_gpu() else {
    1438
                eprintln!("GPU not available, skipping test");
    1439
                return;
    1440
            };
    1441
    1442
            // Large 64×64 matrix (16 tiles)
    1443
            let width = 64;
    1444
            let height = 64;
    1445
            let data: Vec<f32> = vec![1.0; width * height];
    1446
    1447
            let cpu_result = tiled_sum_2d(&data, width, height);
    1448
            let expected = (width * height) as f32;
    1449
    1450
            // Verify CPU is correct
    1451
            assert!((cpu_result - expected).abs() < 1e-3);
    1452
    1453
            if let Ok(gpu_result) = gpu.tiled_sum_2d_gpu(&data, width, height) {
    1454
                let rel_err = (gpu_result - expected).abs() / expected;
    1455
                assert!(
    1456
                    rel_err < 1e-4,
    1457
                    "GPU tiled_sum large matrix: got {}, expected {}, rel_err={}",
    1458
                    gpu_result,
    1459
                    expected,
    1460
                    rel_err
    1461
                );
    1462
            } else {
    1463
                eprintln!("GPU tiled_sum_2d large matrix failed");
    1464
            }
    1465
        }
    1466
    1467
        #[test]
    1468
        fn test_gpu_tiled_max_with_negatives() {
    1469
            let Some(mut gpu) = get_shared_gpu() else {
    1470
                eprintln!("GPU not available, skipping test");
    1471
                return;
    1472
            };
    1473
    1474
            // All negative values - max should be -1.0
    1475
            let data: Vec<f32> = (-100..-1).map(|x| x as f32).collect();
    1476
            let width = 9;
    1477
            let height = 11;
    1478
    1479
            let cpu_result = tiled_max_2d(&data, width, height);
    1480
    1481
            if let Ok(gpu_result) = gpu.tiled_max_2d_gpu(&data, width, height) {
    1482
                assert!(
    1483
                    (gpu_result - cpu_result).abs() < 1e-5,
    1484
                    "GPU vs CPU tiled_max (negatives): gpu={}, cpu={}",
    1485
                    gpu_result,
    1486
                    cpu_result
    1487
                );
    1488
            } else {
    1489
                eprintln!("GPU tiled_max_2d with negatives failed");
    1490
            }
    1491
        }
    1492
    1493
        #[test]
    1494
        fn test_gpu_tiled_min_single_element() {
    1495
            let Some(mut gpu) = get_shared_gpu() else {
    1496
                eprintln!("GPU not available, skipping test");
    1497
                return;
    1498
            };
    1499
    1500
            let data = vec![42.0];
    1501
    1502
            if let Ok(gpu_result) = gpu.tiled_min_2d_gpu(&data, 1, 1) {
    1503
                assert!(
    1504
                    (gpu_result - 42.0).abs() < 1e-5,
    1505
                    "GPU tiled_min single element: got {}, expected 42.0",
    1506
                    gpu_result
    1507
                );
    1508
            } else {
    1509
                eprintln!("GPU tiled_min_2d single element failed");
    1510
            }
    1511
        }
    1512
    }