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
}