/home/noah/src/trueno/src/lib.rs
Line | Count | Source |
1 | | // ============================================================================ |
2 | | // Development-phase lint allows - to be addressed incrementally |
3 | | // ============================================================================ |
4 | | // Allow manual_div_ceil - clearer for block calculations |
5 | | #![allow(clippy::manual_div_ceil)] |
6 | | // Allow manual_is_multiple_of - clearer alignment checks |
7 | | #![allow(clippy::manual_is_multiple_of)] |
8 | | // Allow needless_range_loop - index access is clearer in some SIMD algorithms |
9 | | #![allow(clippy::needless_range_loop)] |
10 | | // Allow empty line after doc comments - formatting preference |
11 | | #![allow(clippy::empty_line_after_doc_comments)] |
12 | | // Allow similar names - semantic distinction is clear |
13 | | #![allow(clippy::similar_names)] |
14 | | // Allow many single char names - standard math/matrix notation |
15 | | #![allow(clippy::many_single_char_names)] |
16 | | // Allow too many arguments - SIMD/compute APIs require many parameters |
17 | | #![allow(clippy::too_many_arguments)] |
18 | | // Allow type complexity - complex SIMD types |
19 | | #![allow(clippy::type_complexity)] |
20 | | // Allow macro metavars in unsafe - necessary for SIMD dispatch macros |
21 | | #![allow(clippy::macro_metavars_in_unsafe)] |
22 | | // Allow missing panics doc - will be added incrementally |
23 | | #![allow(clippy::missing_panics_doc)] |
24 | | // Allow missing errors doc - will be added incrementally |
25 | | #![allow(clippy::missing_errors_doc)] |
26 | | // Allow missing safety doc - will be added incrementally |
27 | | #![allow(clippy::missing_safety_doc)] |
28 | | // Allow excessive precision - SIMD math constants need specific precision |
29 | | #![allow(clippy::excessive_precision)] |
30 | | // Allow unnecessary cast - clearer type annotations in some cases |
31 | | #![allow(clippy::unnecessary_cast)] |
32 | | // Allow cast_possible_truncation - handled in SIMD code |
33 | | #![allow(clippy::cast_possible_truncation)] |
34 | | // Allow cast_sign_loss - handled in SIMD code |
35 | | #![allow(clippy::cast_sign_loss)] |
36 | | // Allow cast_precision_loss - handled in SIMD code |
37 | | #![allow(clippy::cast_precision_loss)] |
38 | | |
39 | | //! Trueno: Multi-Target High-Performance Compute Library |
40 | | //! |
41 | | //! **Trueno** (Spanish: "thunder") provides unified, high-performance compute primitives |
42 | | //! across three execution targets: |
43 | | //! |
44 | | //! 1. **CPU SIMD** - x86 (SSE2/AVX/AVX2/AVX-512), ARM (NEON), WASM (SIMD128) |
45 | | //! 2. **GPU** - Vulkan/Metal/DX12/WebGPU via `wgpu` |
46 | | //! 3. **WebAssembly** - Portable SIMD128 for browser/edge deployment |
47 | | //! |
48 | | //! # Design Principles |
49 | | //! |
50 | | //! - **Write once, optimize everywhere**: Single algorithm, multiple backends |
51 | | //! - **Runtime dispatch**: Auto-select best implementation based on CPU features |
52 | | //! - **Zero unsafe in public API**: Safety via type system, `unsafe` isolated in backends |
53 | | //! - **Benchmarked performance**: Every optimization must prove ≥10% speedup |
54 | | //! - **Extreme TDD**: >90% test coverage, mutation testing, property-based tests |
55 | | //! |
56 | | //! # Quick Start |
57 | | //! |
58 | | //! ```rust |
59 | | //! use trueno::Vector; |
60 | | //! |
61 | | //! let a = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]); |
62 | | //! let b = Vector::from_slice(&[5.0, 6.0, 7.0, 8.0]); |
63 | | //! |
64 | | //! // Auto-selects best backend (AVX2/GPU/WASM) |
65 | | //! let result = a.add(&b).unwrap(); |
66 | | //! assert_eq!(result.as_slice(), &[6.0, 8.0, 10.0, 12.0]); |
67 | | //! ``` |
68 | | |
69 | | pub mod backends; |
70 | | pub mod blis; |
71 | | pub mod brick; |
72 | | pub mod chaos; |
73 | | pub mod eigen; |
74 | | pub mod error; |
75 | | pub mod hardware; |
76 | | pub mod hash; |
77 | | pub mod matrix; |
78 | | pub mod monitor; |
79 | | pub mod simulation; |
80 | | pub mod tiling; |
81 | | pub mod tuner; |
82 | | pub mod vector; |
83 | | |
84 | | pub use eigen::SymmetricEigen; |
85 | | pub use error::{Result, TruenoError}; |
86 | | pub use hash::{hash_bytes, hash_key, hash_keys_batch, hash_keys_batch_with_backend}; |
87 | | pub use matrix::Matrix; |
88 | | pub use monitor::{ |
89 | | cuda_monitor_available, GpuBackend, GpuClockMetrics, GpuDeviceInfo, GpuMemoryMetrics, |
90 | | GpuMetrics, GpuMonitor, GpuPcieMetrics, GpuPowerMetrics, GpuThermalMetrics, GpuUtilization, |
91 | | GpuVendor, MonitorConfig, MonitorError, |
92 | | }; |
93 | | #[cfg(feature = "cuda-monitor")] |
94 | | pub use monitor::{enumerate_cuda_devices, query_cuda_device_info, query_cuda_memory}; |
95 | | pub use vector::Vector; |
96 | | |
97 | | // ComputeBrick exports |
98 | | pub use brick::{ |
99 | | AddOp, AssertionResult, AttentionOp, BrickError, BrickLayer, BrickProfiler, BrickSample, |
100 | | BrickStats, BrickTimer, BrickVerification, ByteBudget, ComputeAssertion, ComputeBackend, |
101 | | ComputeBrick, ComputeOp, DotOp, FusedGateUpOp, FusedGateUpWeights, FusedQKVOp, |
102 | | FusedQKVWeights, MatmulOp, SoftmaxOp, TokenBudget, TokenResult, |
103 | | // QUANT-Q5K: Q5_K and Q6_K quantization formats (llama.cpp compatible) |
104 | | BlockQ5K, BlockQ6K, DotQ5KOp, DotQ6KOp, |
105 | | // CORRECTNESS-011: Divergence detection types |
106 | | KernelChecksum, DivergenceInfo, fnv1a_f32_checksum, |
107 | | // PAR-200: BrickProfiler v2 types |
108 | | BrickId, BrickIdTimer, BrickCategory, BrickBottleneck, CategoryStats, SyncMode, |
109 | | // PAR-201: Execution path graph types |
110 | | ExecutionNodeId, ExecutionNode, EdgeType, ExecutionEdge, ExecutionGraph, PtxRegistry, |
111 | | // TILING-SPEC-001: Tile-level profiling types |
112 | | TileLevel, TileStats, TileTimer, |
113 | | }; |
114 | | |
115 | | // Hardware capability exports (PMAT-447) |
116 | | pub use hardware::{ |
117 | | Bottleneck, CpuCapability, GpuCapability, HardwareCapability, RooflineParams, SimdWidth, |
118 | | GpuBackend as HardwareGpuBackend, default_hardware_path, |
119 | | }; |
120 | | |
121 | | // ML Tuner exports (T-TUNER-003 through T-TUNER-007, GH#80-84) |
122 | | pub use tuner::{ |
123 | | BottleneckClass, BottleneckPrediction, BrickTuner, ConceptDriftStatus, ExperimentSuggestion, |
124 | | FeatureExtractor, KernelClassifier, KernelRecommendation, KernelType, QuantType, RunConfig, |
125 | | ThroughputPrediction, ThroughputRegressor, TrainingSample, TrainingStats, TunerDataCollector, |
126 | | TunerError, TunerFeatures, TunerRecommendation, UserFeedback, |
127 | | }; |
128 | | |
129 | | // Tiling Compute Blocks exports (TILING-SPEC-001) |
130 | | pub use tiling::{ |
131 | | PackingLayout, PrefetchLocality, TcbGeometry, TcbIndexCalculator, TcbLevel, TilingBackend, |
132 | | TilingConfig, TilingError, TilingStats, TiledQ4KMatvec, |
133 | | Q4K_SUPERBLOCK_SIZE, Q4K_SUPERBLOCK_BYTES, |
134 | | optimal_prefetch_distance, pack_a_index, pack_b_index, swizzle_index, |
135 | | }; |
136 | | |
137 | | /// Backend execution target |
138 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
139 | | pub enum Backend { |
140 | | /// Scalar fallback (no SIMD) |
141 | | Scalar, |
142 | | /// SSE2 (x86_64 baseline) |
143 | | SSE2, |
144 | | /// AVX (256-bit) |
145 | | AVX, |
146 | | /// AVX2 (256-bit with FMA) |
147 | | AVX2, |
148 | | /// AVX-512 (512-bit) |
149 | | AVX512, |
150 | | /// ARM NEON |
151 | | NEON, |
152 | | /// WebAssembly SIMD128 |
153 | | WasmSIMD, |
154 | | /// GPU compute (wgpu) |
155 | | GPU, |
156 | | /// Auto-select best available |
157 | | Auto, |
158 | | } |
159 | | |
160 | | impl Backend { |
161 | | /// Select the best available backend for the current platform |
162 | | /// |
163 | | /// This is a convenience wrapper around `select_best_available_backend()` |
164 | 10 | pub fn select_best() -> Self { |
165 | 10 | select_best_available_backend() |
166 | 10 | } |
167 | | } |
168 | | |
169 | | /// Operation complexity for GPU dispatch eligibility |
170 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
171 | | pub enum OpComplexity { |
172 | | /// Simple operations (add, mul) - prefer SIMD unless very large |
173 | | Low = 0, |
174 | | /// Moderate operations (dot, reduce) - GPU beneficial at 100K+ |
175 | | Medium = 1, |
176 | | /// Complex operations (matmul, convolution) - GPU beneficial at 10K+ |
177 | | High = 2, |
178 | | } |
179 | | |
180 | | /// Operation type for SIMD backend selection |
181 | | /// |
182 | | /// Based on AVX-512 performance analysis (see AVX512_ANALYSIS.md), operations are |
183 | | /// categorized by their memory vs compute characteristics to guide optimal backend selection. |
184 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
185 | | pub enum OperationType { |
186 | | /// Memory-bound operations (add, sub, mul, scale, div) |
187 | | /// |
188 | | /// These operations perform minimal computation per memory access (arithmetic intensity < 1 op/byte). |
189 | | /// Prefer AVX2 over AVX-512 due to memory bandwidth bottleneck. |
190 | | /// |
191 | | /// AVX-512 performance: 0.67-1.20x scalar (often slower!) |
192 | | /// AVX2 performance: 1.0-1.2x scalar |
193 | | MemoryBound, |
194 | | |
195 | | /// Compute-bound operations (dot, max, min, argmax, argmin) |
196 | | /// |
197 | | /// These operations perform significant computation per memory access (arithmetic intensity > 1 op/byte). |
198 | | /// AVX-512 excels due to wider SIMD parallelism. |
199 | | /// |
200 | | /// AVX-512 performance: 7-14x scalar (validated) |
201 | | /// AVX2 performance: 4-12x scalar (validated) |
202 | | ComputeBound, |
203 | | |
204 | | /// Mixed operations (fma, sqrt, exp, sigmoid, activations) |
205 | | /// |
206 | | /// Performance depends on data size and hardware. |
207 | | /// Use size-based heuristics or default to AVX2 for safety. |
208 | | Mixed, |
209 | | } |
210 | | |
211 | | /// Detect best SIMD backend for x86/x86_64 platforms |
212 | | /// |
213 | | /// **IMPORTANT**: Prefers AVX2 over AVX-512 by default based on performance analysis. |
214 | | /// |
215 | | /// AVX-512 is **NOT** universally faster - it causes 10-33% slowdown for memory-bound |
216 | | /// operations (add, mul, sub) due to memory bandwidth bottleneck and thermal throttling. |
217 | | /// See AVX512_ANALYSIS.md for detailed benchmarking results. |
218 | | /// |
219 | | /// For operation-specific backend selection, use `select_backend_for_operation()`. |
220 | | #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] |
221 | 1 | fn detect_x86_backend() -> Backend { |
222 | | // Prefer AVX2 over AVX-512 for safety (AVX-512 causes regressions for memory-bound ops) |
223 | 1 | if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") { |
224 | 1 | return Backend::AVX2; |
225 | 0 | } |
226 | | // Note: AVX-512 is intentionally NOT checked here |
227 | | // Use select_backend_for_operation(OperationType::ComputeBound) for AVX-512 |
228 | 0 | if is_x86_feature_detected!("avx") { |
229 | 0 | return Backend::AVX; |
230 | 0 | } |
231 | 0 | if is_x86_feature_detected!("sse2") { |
232 | 0 | return Backend::SSE2; |
233 | 0 | } |
234 | 0 | Backend::Scalar |
235 | 1 | } |
236 | | |
237 | | /// Detect best SIMD backend for ARM platforms |
238 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
239 | | fn detect_arm_backend() -> Backend { |
240 | | #[cfg(target_feature = "neon")] |
241 | | { |
242 | | Backend::NEON |
243 | | } |
244 | | #[cfg(not(target_feature = "neon"))] |
245 | | { |
246 | | Backend::Scalar |
247 | | } |
248 | | } |
249 | | |
250 | | /// Detect best SIMD backend for WebAssembly |
251 | | #[cfg(target_arch = "wasm32")] |
252 | | fn detect_wasm_backend() -> Backend { |
253 | | #[cfg(target_feature = "simd128")] |
254 | | { |
255 | | Backend::WasmSIMD |
256 | | } |
257 | | #[cfg(not(target_feature = "simd128"))] |
258 | | { |
259 | | Backend::Scalar |
260 | | } |
261 | | } |
262 | | |
263 | | /// Select the best available backend for the current platform |
264 | | /// |
265 | | /// This function performs runtime CPU feature detection and selects the most |
266 | | /// optimized backend available. The selection follows this priority: |
267 | | /// |
268 | | /// **x86/x86_64**: |
269 | | /// 1. AVX-512 (if `avx512f` feature detected) |
270 | | /// 2. AVX2 (if `avx2` and `fma` features detected) |
271 | | /// 3. AVX (if `avx` feature detected) |
272 | | /// 4. SSE2 (baseline for x86_64) |
273 | | /// 5. Scalar (fallback) |
274 | | /// |
275 | | /// **ARM**: |
276 | | /// 1. NEON (if available) |
277 | | /// 2. Scalar (fallback) |
278 | | /// |
279 | | /// **WASM**: SIMD128 (if available), else Scalar |
280 | | /// |
281 | | /// **Other platforms**: Scalar |
282 | | /// |
283 | | /// # Returns |
284 | | /// |
285 | | /// The most optimized backend available on this CPU/platform |
286 | | /// |
287 | | /// # Examples |
288 | | /// |
289 | | /// ``` |
290 | | /// use trueno::select_best_available_backend; |
291 | | /// |
292 | | /// let backend = select_best_available_backend(); |
293 | | /// println!("Using backend: {:?}", backend); |
294 | | /// ``` |
295 | 257k | pub fn select_best_available_backend() -> Backend { |
296 | | // Cache backend selection using OnceLock to avoid repeated CPU feature detection |
297 | | // This eliminates 3-5% overhead from calling is_x86_feature_detected!() repeatedly |
298 | | static BEST_BACKEND: std::sync::OnceLock<Backend> = std::sync::OnceLock::new(); |
299 | | |
300 | 257k | *BEST_BACKEND.get_or_init(|| {1 |
301 | | #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] |
302 | | { |
303 | 1 | detect_x86_backend() |
304 | | } |
305 | | |
306 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
307 | | { |
308 | | detect_arm_backend() |
309 | | } |
310 | | |
311 | | #[cfg(target_arch = "wasm32")] |
312 | | { |
313 | | detect_wasm_backend() |
314 | | } |
315 | | |
316 | | #[cfg(not(any( |
317 | | target_arch = "x86_64", |
318 | | target_arch = "x86", |
319 | | target_arch = "aarch64", |
320 | | target_arch = "arm", |
321 | | target_arch = "wasm32" |
322 | | )))] |
323 | | { |
324 | | Backend::Scalar |
325 | | } |
326 | 1 | }) |
327 | 257k | } |
328 | | |
329 | | /// Select the optimal backend for a specific operation type |
330 | | /// |
331 | | /// This function considers the memory vs compute characteristics of operations |
332 | | /// to select the backend that will provide the best performance. Based on |
333 | | /// comprehensive benchmarking (see AVX512_ANALYSIS.md), AVX-512 is avoided |
334 | | /// for memory-bound operations where it causes 10-33% performance degradation. |
335 | | /// |
336 | | /// # Operation Classification |
337 | | /// |
338 | | /// - **MemoryBound**: add, sub, mul, div, scale, abs, clamp, lerp, relu |
339 | | /// - Prefer AVX2 (1.0-1.2x scalar) over AVX-512 (0.67-1.20x scalar) |
340 | | /// - Memory bandwidth bottleneck limits wider SIMD benefit |
341 | | /// |
342 | | /// - **ComputeBound**: dot, max, min, argmax, argmin, norm_l1, norm_l2, norm_linf |
343 | | /// - Prefer AVX-512 (7-14x scalar) over AVX2 (4-12x scalar) |
344 | | /// - High arithmetic intensity benefits from wider SIMD |
345 | | /// |
346 | | /// - **Mixed**: fma, sqrt, exp, ln, sigmoid, tanh, gelu, swish |
347 | | /// - Default to AVX2 for safety (avoids AVX-512 thermal throttling) |
348 | | /// - Size-based heuristics could improve this in future |
349 | | /// |
350 | | /// # Backend Selection Priority |
351 | | /// |
352 | | /// **For MemoryBound operations**: |
353 | | /// 1. AVX2 (if available) - BEST for memory-bound |
354 | | /// 2. SSE2 (x86_64 baseline) |
355 | | /// 3. AVX-512 (AVOIDED - causes slowdown) |
356 | | /// 4. NEON (ARM) |
357 | | /// 5. WASM SIMD128 |
358 | | /// 6. Scalar (fallback) |
359 | | /// |
360 | | /// **For ComputeBound operations**: |
361 | | /// 1. AVX-512 (if available) - BEST for compute-bound |
362 | | /// 2. AVX2 |
363 | | /// 3. SSE2 |
364 | | /// 4. NEON (ARM) |
365 | | /// 5. WASM SIMD128 |
366 | | /// 6. Scalar (fallback) |
367 | | /// |
368 | | /// # Arguments |
369 | | /// |
370 | | /// * `op_type` - The type of operation being performed |
371 | | /// |
372 | | /// # Returns |
373 | | /// |
374 | | /// The optimal backend for the given operation type |
375 | | /// |
376 | | /// # Examples |
377 | | /// |
378 | | /// ``` |
379 | | /// use trueno::{select_backend_for_operation, OperationType}; |
380 | | /// |
381 | | /// // Memory-bound operation - prefers AVX2 over AVX-512 |
382 | | /// let backend = select_backend_for_operation(OperationType::MemoryBound); |
383 | | /// |
384 | | /// // Compute-bound operation - uses AVX-512 if available |
385 | | /// let backend = select_backend_for_operation(OperationType::ComputeBound); |
386 | | /// ``` |
387 | | /// |
388 | | /// # Performance Impact |
389 | | /// |
390 | | /// Using operation-aware backend selection fixes performance regressions: |
391 | | /// - mul with AVX-512: 0.67x → 1.0x (use AVX2 instead) |
392 | | /// - sub with AVX-512: 0.87x → 1.0x (use AVX2 instead) |
393 | | /// - dot with AVX-512: 7.89x (keep AVX-512) |
394 | 0 | pub fn select_backend_for_operation(op_type: OperationType) -> Backend { |
395 | | // Allow unused on non-x86 architectures |
396 | 0 | let _ = &op_type; |
397 | | |
398 | | #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] |
399 | | { |
400 | 0 | select_x86_backend_for_operation(op_type) |
401 | | } |
402 | | |
403 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
404 | | { |
405 | | detect_arm_backend() |
406 | | } |
407 | | |
408 | | #[cfg(target_arch = "wasm32")] |
409 | | { |
410 | | detect_wasm_backend() |
411 | | } |
412 | | |
413 | | #[cfg(not(any( |
414 | | target_arch = "x86_64", |
415 | | target_arch = "x86", |
416 | | target_arch = "aarch64", |
417 | | target_arch = "arm", |
418 | | target_arch = "wasm32" |
419 | | )))] |
420 | | { |
421 | | Backend::Scalar |
422 | | } |
423 | 0 | } |
424 | | |
425 | | /// Select the best x86 backend based on operation type and available features. |
426 | | /// |
427 | | /// Separated from `select_backend_for_operation` to reduce cyclomatic complexity. |
428 | | #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] |
429 | 0 | fn select_x86_backend_for_operation(op_type: OperationType) -> Backend { |
430 | | use std::arch::is_x86_feature_detected; |
431 | | |
432 | | // Check for AVX-512 (only for compute-bound operations) |
433 | 0 | let use_avx512 = op_type == OperationType::ComputeBound && is_x86_feature_detected!("avx512f"); |
434 | 0 | if use_avx512 { |
435 | 0 | return Backend::AVX512; |
436 | 0 | } |
437 | | |
438 | | // AVX2 with FMA is preferred for most operations |
439 | 0 | if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") { |
440 | 0 | return Backend::AVX2; |
441 | 0 | } |
442 | | |
443 | | // Fallback chain: AVX -> SSE2 -> Scalar |
444 | 0 | if is_x86_feature_detected!("avx") { |
445 | 0 | return Backend::AVX; |
446 | 0 | } |
447 | 0 | if is_x86_feature_detected!("sse2") { |
448 | 0 | return Backend::SSE2; |
449 | 0 | } |
450 | | |
451 | 0 | Backend::Scalar |
452 | 0 | } |
453 | | |
454 | | #[cfg(test)] |
455 | | mod tests { |
456 | | use super::*; |
457 | | |
458 | | #[test] |
459 | | fn test_backend_enum() { |
460 | | assert_eq!(Backend::Scalar, Backend::Scalar); |
461 | | assert_ne!(Backend::Scalar, Backend::AVX2); |
462 | | } |
463 | | |
464 | | #[test] |
465 | | fn test_op_complexity_ordering() { |
466 | | assert!(OpComplexity::Low < OpComplexity::Medium); |
467 | | assert!(OpComplexity::Medium < OpComplexity::High); |
468 | | } |
469 | | |
470 | | #[test] |
471 | | fn test_select_best_available_backend() { |
472 | | let backend = select_best_available_backend(); |
473 | | |
474 | | // On x86_64, we should get at least SSE2 (baseline for x86_64) |
475 | | // or a more advanced SIMD backend if available |
476 | | #[cfg(target_arch = "x86_64")] |
477 | | { |
478 | | // x86_64 baseline is SSE2, so we should never get Scalar on x86_64 |
479 | | assert_ne!(backend, Backend::Scalar); |
480 | | // Verify it's one of the x86 SIMD backends |
481 | | assert!(matches!( |
482 | | backend, |
483 | | Backend::SSE2 | Backend::AVX | Backend::AVX2 | Backend::AVX512 |
484 | | )); |
485 | | } |
486 | | |
487 | | // On other platforms, we might get Scalar or platform-specific SIMD |
488 | | #[cfg(not(target_arch = "x86_64"))] |
489 | | { |
490 | | // Just verify we got a valid backend |
491 | | assert!(matches!( |
492 | | backend, |
493 | | Backend::Scalar |
494 | | | Backend::SSE2 |
495 | | | Backend::AVX |
496 | | | Backend::AVX2 |
497 | | | Backend::AVX512 |
498 | | | Backend::NEON |
499 | | | Backend::WasmSIMD |
500 | | )); |
501 | | } |
502 | | } |
503 | | |
504 | | #[test] |
505 | | fn test_backend_selection_is_deterministic() { |
506 | | // Backend selection should be deterministic (same result on multiple calls) |
507 | | let backend1 = select_best_available_backend(); |
508 | | let backend2 = select_best_available_backend(); |
509 | | assert_eq!(backend1, backend2); |
510 | | } |
511 | | |
512 | | #[test] |
513 | | fn test_backend_selection_is_cached() { |
514 | | // Verify backend selection is cached (OnceLock) |
515 | | // Multiple calls should return the same backend without re-detection |
516 | | let backend1 = select_best_available_backend(); |
517 | | |
518 | | // Call 1000 times to ensure caching is working |
519 | | // If not cached, this would be significantly slower |
520 | | for _ in 0..1000 { |
521 | | let backend = select_best_available_backend(); |
522 | | assert_eq!(backend, backend1, "Backend selection must be consistent"); |
523 | | } |
524 | | } |
525 | | |
526 | | #[test] |
527 | | fn test_backend_select_best() { |
528 | | // Test Backend::select_best() method |
529 | | let backend = Backend::select_best(); |
530 | | assert_eq!(backend, select_best_available_backend()); |
531 | | } |
532 | | |
533 | | #[test] |
534 | | fn test_backend_variants() { |
535 | | // Test all backend variants |
536 | | let backends = vec![ |
537 | | Backend::Scalar, |
538 | | Backend::SSE2, |
539 | | Backend::AVX, |
540 | | Backend::AVX2, |
541 | | Backend::AVX512, |
542 | | Backend::NEON, |
543 | | Backend::WasmSIMD, |
544 | | Backend::GPU, |
545 | | Backend::Auto, |
546 | | ]; |
547 | | |
548 | | // Verify all variants are distinct |
549 | | for (i, backend1) in backends.iter().enumerate() { |
550 | | for (j, backend2) in backends.iter().enumerate() { |
551 | | if i == j { |
552 | | assert_eq!(backend1, backend2); |
553 | | } else { |
554 | | assert_ne!(backend1, backend2); |
555 | | } |
556 | | } |
557 | | } |
558 | | } |
559 | | |
560 | | #[test] |
561 | | fn test_backend_debug() { |
562 | | // Verify Debug trait works |
563 | | let backend = Backend::AVX2; |
564 | | let debug_str = format!("{:?}", backend); |
565 | | assert!(debug_str.contains("AVX2")); |
566 | | |
567 | | let backend = Backend::Auto; |
568 | | let debug_str = format!("{:?}", backend); |
569 | | assert!(debug_str.contains("Auto")); |
570 | | } |
571 | | |
572 | | #[test] |
573 | | fn test_backend_clone() { |
574 | | let backend = Backend::AVX2; |
575 | | #[allow(clippy::clone_on_copy)] |
576 | | let cloned = backend.clone(); |
577 | | assert_eq!(backend, cloned); |
578 | | } |
579 | | |
580 | | #[test] |
581 | | fn test_backend_copy() { |
582 | | let backend = Backend::SSE2; |
583 | | let copied = backend; |
584 | | assert_eq!(backend, copied); |
585 | | } |
586 | | |
587 | | #[test] |
588 | | fn test_op_complexity_values() { |
589 | | assert_eq!(OpComplexity::Low as i32, 0); |
590 | | assert_eq!(OpComplexity::Medium as i32, 1); |
591 | | assert_eq!(OpComplexity::High as i32, 2); |
592 | | } |
593 | | |
594 | | #[test] |
595 | | fn test_op_complexity_ord() { |
596 | | // Test PartialOrd |
597 | | assert!(OpComplexity::Low < OpComplexity::Medium); |
598 | | assert!(OpComplexity::Medium < OpComplexity::High); |
599 | | assert!(OpComplexity::Low < OpComplexity::High); |
600 | | |
601 | | // Test Ord |
602 | | use std::cmp::Ordering; |
603 | | assert_eq!(OpComplexity::Low.cmp(&OpComplexity::Medium), Ordering::Less); |
604 | | assert_eq!( |
605 | | OpComplexity::Medium.cmp(&OpComplexity::High), |
606 | | Ordering::Less |
607 | | ); |
608 | | assert_eq!( |
609 | | OpComplexity::High.cmp(&OpComplexity::Medium), |
610 | | Ordering::Greater |
611 | | ); |
612 | | assert_eq!(OpComplexity::Low.cmp(&OpComplexity::Low), Ordering::Equal); |
613 | | } |
614 | | |
615 | | #[test] |
616 | | fn test_op_complexity_eq() { |
617 | | assert_eq!(OpComplexity::Low, OpComplexity::Low); |
618 | | assert_eq!(OpComplexity::Medium, OpComplexity::Medium); |
619 | | assert_eq!(OpComplexity::High, OpComplexity::High); |
620 | | assert_ne!(OpComplexity::Low, OpComplexity::High); |
621 | | } |
622 | | |
623 | | #[test] |
624 | | fn test_op_complexity_debug() { |
625 | | let complexity = OpComplexity::Medium; |
626 | | let debug_str = format!("{:?}", complexity); |
627 | | assert!(debug_str.contains("Medium")); |
628 | | } |
629 | | |
630 | | #[test] |
631 | | fn test_op_complexity_clone() { |
632 | | let complexity = OpComplexity::High; |
633 | | #[allow(clippy::clone_on_copy)] |
634 | | let cloned = complexity.clone(); |
635 | | assert_eq!(complexity, cloned); |
636 | | } |
637 | | |
638 | | #[test] |
639 | | fn test_op_complexity_copy() { |
640 | | let complexity = OpComplexity::Low; |
641 | | let copied = complexity; |
642 | | assert_eq!(complexity, copied); |
643 | | } |
644 | | |
645 | | #[test] |
646 | | fn test_trueno_error_reexport() { |
647 | | // Verify error types are re-exported correctly |
648 | | let _: Result<()> = Ok(()); |
649 | | let err: Result<()> = Err(TruenoError::EmptyVector); |
650 | | assert!(err.is_err()); |
651 | | } |
652 | | |
653 | | #[test] |
654 | | fn test_vector_reexport() { |
655 | | // Verify Vector is re-exported correctly |
656 | | let v = Vector::from_slice(&[1.0, 2.0, 3.0]); |
657 | | assert_eq!(v.len(), 3); |
658 | | } |
659 | | |
660 | | #[test] |
661 | | fn test_matrix_reexport() { |
662 | | // Verify Matrix is re-exported correctly |
663 | | let m = Matrix::zeros(2, 2); |
664 | | assert_eq!(m.rows(), 2); |
665 | | assert_eq!(m.cols(), 2); |
666 | | } |
667 | | |
668 | | #[cfg(target_arch = "x86_64")] |
669 | | #[test] |
670 | | fn test_detect_x86_backend() { |
671 | | let backend = detect_x86_backend(); |
672 | | // On x86_64, we should get at least SSE2 |
673 | | assert!(matches!( |
674 | | backend, |
675 | | Backend::SSE2 | Backend::AVX | Backend::AVX2 |
676 | | )); |
677 | | // Should NOT return AVX-512 (intentionally avoided for safety) |
678 | | assert_ne!(backend, Backend::AVX512); |
679 | | } |
680 | | |
681 | | #[test] |
682 | | fn test_operation_type_enum() { |
683 | | // Verify OperationType variants are distinct |
684 | | assert_ne!(OperationType::MemoryBound, OperationType::ComputeBound); |
685 | | assert_ne!(OperationType::MemoryBound, OperationType::Mixed); |
686 | | assert_ne!(OperationType::ComputeBound, OperationType::Mixed); |
687 | | } |
688 | | |
689 | | #[cfg(target_arch = "x86_64")] |
690 | | #[test] |
691 | | fn test_select_backend_for_memory_bound_prefers_avx2() { |
692 | | let backend = select_backend_for_operation(OperationType::MemoryBound); |
693 | | |
694 | | // Should prefer AVX2 over AVX-512 for memory-bound operations |
695 | | // (Based on AVX-512 performance analysis showing 0.67-1.01x scalar) |
696 | | if is_x86_feature_detected!("avx2") { |
697 | | assert_eq!(backend, Backend::AVX2); |
698 | | } else if is_x86_feature_detected!("avx") { |
699 | | assert_eq!(backend, Backend::AVX); |
700 | | } else if is_x86_feature_detected!("sse2") { |
701 | | assert_eq!(backend, Backend::SSE2); |
702 | | } else { |
703 | | assert_eq!(backend, Backend::Scalar); |
704 | | } |
705 | | |
706 | | // Critical: Should NEVER return AVX-512 for memory-bound |
707 | | assert_ne!(backend, Backend::AVX512); |
708 | | } |
709 | | |
710 | | #[cfg(target_arch = "x86_64")] |
711 | | #[test] |
712 | | fn test_select_backend_for_compute_bound_allows_avx512() { |
713 | | let backend = select_backend_for_operation(OperationType::ComputeBound); |
714 | | |
715 | | // Should prefer AVX-512 for compute-bound operations where it excels (7-14x scalar) |
716 | | if is_x86_feature_detected!("avx512f") { |
717 | | assert_eq!(backend, Backend::AVX512); |
718 | | } else if is_x86_feature_detected!("avx2") { |
719 | | assert_eq!(backend, Backend::AVX2); |
720 | | } else if is_x86_feature_detected!("avx") { |
721 | | assert_eq!(backend, Backend::AVX); |
722 | | } else if is_x86_feature_detected!("sse2") { |
723 | | assert_eq!(backend, Backend::SSE2); |
724 | | } else { |
725 | | assert_eq!(backend, Backend::Scalar); |
726 | | } |
727 | | } |
728 | | |
729 | | #[cfg(target_arch = "x86_64")] |
730 | | #[test] |
731 | | fn test_select_backend_for_mixed_prefers_avx2() { |
732 | | let backend = select_backend_for_operation(OperationType::Mixed); |
733 | | |
734 | | // Mixed operations should default to AVX2 for safety (avoid AVX-512 thermal throttling) |
735 | | if is_x86_feature_detected!("avx2") { |
736 | | assert_eq!(backend, Backend::AVX2); |
737 | | } else if is_x86_feature_detected!("avx") { |
738 | | assert_eq!(backend, Backend::AVX); |
739 | | } else if is_x86_feature_detected!("sse2") { |
740 | | assert_eq!(backend, Backend::SSE2); |
741 | | } else { |
742 | | assert_eq!(backend, Backend::Scalar); |
743 | | } |
744 | | |
745 | | // Should NOT return AVX-512 for mixed operations (safety first) |
746 | | assert_ne!(backend, Backend::AVX512); |
747 | | } |
748 | | |
749 | | #[cfg(target_arch = "x86_64")] |
750 | | #[test] |
751 | | fn test_default_backend_selection_avoids_avx512() { |
752 | | // The default backend selection (detect_x86_backend) should avoid AVX-512 |
753 | | let default_backend = select_best_available_backend(); |
754 | | |
755 | | // Even on CPUs with AVX-512, default selection should prefer AVX2 |
756 | | if is_x86_feature_detected!("avx2") { |
757 | | assert_eq!(default_backend, Backend::AVX2); |
758 | | } |
759 | | |
760 | | // Verify AVX-512 is NOT returned by default |
761 | | assert_ne!(default_backend, Backend::AVX512); |
762 | | } |
763 | | |
764 | | #[cfg(target_arch = "x86_64")] |
765 | | #[test] |
766 | | fn test_backend_selection_consistency() { |
767 | | // Memory-bound and Mixed should return same backend (AVX2-first) |
768 | | let memory_backend = select_backend_for_operation(OperationType::MemoryBound); |
769 | | let mixed_backend = select_backend_for_operation(OperationType::Mixed); |
770 | | |
771 | | assert_eq!(memory_backend, mixed_backend); |
772 | | |
773 | | // Compute-bound may differ (allows AVX-512) |
774 | | let compute_backend = select_backend_for_operation(OperationType::ComputeBound); |
775 | | |
776 | | // If AVX-512 is available, compute backend should be different |
777 | | if is_x86_feature_detected!("avx512f") { |
778 | | assert_ne!(compute_backend, memory_backend); |
779 | | assert_eq!(compute_backend, Backend::AVX512); |
780 | | } else { |
781 | | // Without AVX-512, all should be the same |
782 | | assert_eq!(compute_backend, memory_backend); |
783 | | } |
784 | | } |
785 | | |
786 | | #[cfg(not(target_arch = "x86_64"))] |
787 | | #[test] |
788 | | fn test_select_backend_for_operation_non_x86() { |
789 | | // On non-x86 platforms, all operation types should return platform-specific backend |
790 | | let memory = select_backend_for_operation(OperationType::MemoryBound); |
791 | | let compute = select_backend_for_operation(OperationType::ComputeBound); |
792 | | let mixed = select_backend_for_operation(OperationType::Mixed); |
793 | | |
794 | | // All should return the same backend (ARM NEON, WASM SIMD, or Scalar) |
795 | | assert_eq!(memory, compute); |
796 | | assert_eq!(memory, mixed); |
797 | | |
798 | | #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] |
799 | | { |
800 | | #[cfg(target_feature = "neon")] |
801 | | assert_eq!(memory, Backend::NEON); |
802 | | } |
803 | | |
804 | | #[cfg(target_arch = "wasm32")] |
805 | | { |
806 | | #[cfg(target_feature = "simd128")] |
807 | | assert_eq!(memory, Backend::WasmSIMD); |
808 | | } |
809 | | } |
810 | | |
811 | | // ======================================================================== |
812 | | // Additional Coverage Tests |
813 | | // ======================================================================== |
814 | | |
815 | | #[test] |
816 | | fn test_operation_type_debug() { |
817 | | let mem = OperationType::MemoryBound; |
818 | | let debug_str = format!("{:?}", mem); |
819 | | assert!(debug_str.contains("MemoryBound")); |
820 | | |
821 | | let compute = OperationType::ComputeBound; |
822 | | let debug_str = format!("{:?}", compute); |
823 | | assert!(debug_str.contains("ComputeBound")); |
824 | | |
825 | | let mixed = OperationType::Mixed; |
826 | | let debug_str = format!("{:?}", mixed); |
827 | | assert!(debug_str.contains("Mixed")); |
828 | | } |
829 | | |
830 | | #[test] |
831 | | fn test_operation_type_clone() { |
832 | | let op_type = OperationType::ComputeBound; |
833 | | #[allow(clippy::clone_on_copy)] |
834 | | let cloned = op_type.clone(); |
835 | | assert_eq!(op_type, cloned); |
836 | | } |
837 | | |
838 | | #[test] |
839 | | fn test_operation_type_copy() { |
840 | | let op_type = OperationType::Mixed; |
841 | | let copied = op_type; |
842 | | assert_eq!(op_type, copied); |
843 | | } |
844 | | |
845 | | #[test] |
846 | | fn test_operation_type_equality() { |
847 | | assert_eq!(OperationType::MemoryBound, OperationType::MemoryBound); |
848 | | assert_eq!(OperationType::ComputeBound, OperationType::ComputeBound); |
849 | | assert_eq!(OperationType::Mixed, OperationType::Mixed); |
850 | | } |
851 | | |
852 | | #[test] |
853 | | fn test_backend_all_variants_debug() { |
854 | | // Test Debug for all backend variants |
855 | | assert!(format!("{:?}", Backend::Scalar).contains("Scalar")); |
856 | | assert!(format!("{:?}", Backend::SSE2).contains("SSE2")); |
857 | | assert!(format!("{:?}", Backend::AVX).contains("AVX")); |
858 | | assert!(format!("{:?}", Backend::AVX512).contains("AVX512")); |
859 | | assert!(format!("{:?}", Backend::NEON).contains("NEON")); |
860 | | assert!(format!("{:?}", Backend::WasmSIMD).contains("WasmSIMD")); |
861 | | assert!(format!("{:?}", Backend::GPU).contains("GPU")); |
862 | | } |
863 | | |
864 | | #[test] |
865 | | fn test_symmetric_eigen_reexport() { |
866 | | // Verify SymmetricEigen is re-exported correctly |
867 | | // Just verify it's accessible |
868 | | let _ = std::mem::size_of::<SymmetricEigen>(); |
869 | | } |
870 | | |
871 | | #[test] |
872 | | fn test_hash_reexport() { |
873 | | // Verify hash functions are re-exported correctly |
874 | | let result = hash_bytes(b"test"); |
875 | | assert_ne!(result, 0); |
876 | | |
877 | | let key_hash = hash_key("test_key"); |
878 | | assert_ne!(key_hash, 0); |
879 | | |
880 | | let keys = ["a", "b", "c", "d"]; |
881 | | let batch_result = hash_keys_batch(&keys); |
882 | | assert_eq!(batch_result.len(), 4); |
883 | | } |
884 | | } |