/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 | | } |