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/hardware.rs
Line
Count
Source
1
//! Hardware Capability Detection (PMAT-447)
2
//!
3
//! Detects CPU SIMD capabilities, GPU presence, and calculates
4
//! theoretical peak performance for roofline analysis.
5
//!
6
//! Integrates with `pmat brick-score` for hardware-aware profiling.
7
8
use serde::{Deserialize, Serialize};
9
use std::fs;
10
use std::path::Path;
11
12
/// SIMD instruction set width
13
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14
pub enum SimdWidth {
15
    /// No SIMD (scalar)
16
    Scalar,
17
    /// ARM NEON (128-bit, 4×f32)
18
    Neon128,
19
    /// SSE2 (128-bit, 4×f32)
20
    Sse2,
21
    /// AVX2 (256-bit, 8×f32)
22
    Avx2,
23
    /// AVX-512 (512-bit, 16×f32)
24
    Avx512,
25
    /// WebAssembly SIMD (128-bit, 4×f32)
26
    WasmSimd128,
27
}
28
29
impl SimdWidth {
30
    /// Number of f32 lanes
31
0
    pub fn lanes(&self) -> usize {
32
0
        match self {
33
0
            SimdWidth::Scalar => 1,
34
0
            SimdWidth::Neon128 | SimdWidth::Sse2 | SimdWidth::WasmSimd128 => 4,
35
0
            SimdWidth::Avx2 => 8,
36
0
            SimdWidth::Avx512 => 16,
37
        }
38
0
    }
39
40
    /// Bit width
41
0
    pub fn bits(&self) -> usize {
42
0
        self.lanes() * 32
43
0
    }
44
45
    /// Typical speedup factor for compute-bound operations
46
0
    pub fn compute_speedup(&self) -> f64 {
47
0
        match self {
48
0
            SimdWidth::Scalar => 1.0,
49
0
            SimdWidth::Neon128 | SimdWidth::Sse2 | SimdWidth::WasmSimd128 => 4.0,
50
0
            SimdWidth::Avx2 => 10.0,  // 8-12x measured in trueno-zram
51
0
            SimdWidth::Avx512 => 12.0, // 8-13x measured
52
        }
53
0
    }
54
}
55
56
/// GPU compute backend
57
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58
pub enum GpuBackend {
59
    /// No GPU available
60
    None,
61
    /// NVIDIA CUDA
62
    Cuda,
63
    /// WebGPU (cross-platform)
64
    Wgpu,
65
    /// Apple Metal
66
    Metal,
67
    /// Vulkan compute
68
    Vulkan,
69
}
70
71
/// CPU capabilities
72
#[derive(Debug, Clone, Serialize, Deserialize)]
73
pub struct CpuCapability {
74
    /// CPU vendor (Intel, AMD, Apple, etc.)
75
    pub vendor: String,
76
    /// CPU model name
77
    pub model: String,
78
    /// Number of physical cores
79
    pub cores: usize,
80
    /// Number of logical threads
81
    pub threads: usize,
82
    /// Best available SIMD width
83
    pub simd: SimdWidth,
84
    /// Base frequency in GHz
85
    pub base_freq_ghz: f64,
86
    /// Theoretical peak GFLOP/s (FMA)
87
    pub peak_gflops: f64,
88
    /// Memory bandwidth in GB/s (estimated)
89
    pub memory_bw_gbps: f64,
90
}
91
92
/// GPU capabilities
93
#[derive(Debug, Clone, Serialize, Deserialize)]
94
pub struct GpuCapability {
95
    /// GPU vendor
96
    pub vendor: String,
97
    /// GPU model name
98
    pub model: String,
99
    /// Compute backend
100
    pub backend: GpuBackend,
101
    /// CUDA compute capability (e.g., "8.9" for RTX 4090)
102
    pub compute_capability: Option<String>,
103
    /// Peak FP32 TFLOP/s
104
    pub peak_tflops_fp32: f64,
105
    /// Peak Tensor Core TFLOP/s (NVIDIA only)
106
    pub peak_tflops_tensor: Option<f64>,
107
    /// Memory bandwidth in GB/s
108
    pub memory_bw_gbps: f64,
109
    /// VRAM in GB
110
    pub vram_gb: f64,
111
}
112
113
/// Complete hardware capability profile
114
#[derive(Debug, Clone, Serialize, Deserialize)]
115
pub struct HardwareCapability {
116
    /// Detection timestamp
117
    pub timestamp: String,
118
    /// Hostname
119
    pub hostname: String,
120
    /// CPU capabilities
121
    pub cpu: CpuCapability,
122
    /// GPU capabilities (if present)
123
    pub gpu: Option<GpuCapability>,
124
    /// Roofline model parameters
125
    pub roofline: RooflineParams,
126
    /// PMAT-452: Byte budget configuration for compression/I/O workloads
127
    #[serde(default)]
128
    pub byte_budget: Option<crate::brick::ByteBudget>,
129
}
130
131
/// Roofline model parameters
132
#[derive(Debug, Clone, Serialize, Deserialize)]
133
pub struct RooflineParams {
134
    /// CPU arithmetic intensity threshold (GFLOP/s ÷ GB/s)
135
    pub cpu_arithmetic_intensity: f64,
136
    /// GPU arithmetic intensity threshold
137
    pub gpu_arithmetic_intensity: Option<f64>,
138
}
139
140
impl HardwareCapability {
141
    /// Detect hardware capabilities at runtime
142
0
    pub fn detect() -> Self {
143
0
        let cpu = detect_cpu();
144
0
        let gpu = detect_gpu();
145
146
0
        let cpu_ai = cpu.peak_gflops / cpu.memory_bw_gbps;
147
0
        let gpu_ai = gpu.as_ref().map(|g| g.peak_tflops_fp32 * 1000.0 / g.memory_bw_gbps);
148
        // PMAT-452: Extract memory bandwidth before moving cpu
149
0
        let byte_budget_throughput = cpu.memory_bw_gbps.min(25.0);
150
151
        HardwareCapability {
152
0
            timestamp: chrono::Utc::now().to_rfc3339(),
153
0
            hostname: hostname::get()
154
0
                .map(|h| h.to_string_lossy().to_string())
155
0
                .unwrap_or_else(|_| "unknown".to_string()),
156
0
            cpu,
157
0
            gpu,
158
0
            roofline: RooflineParams {
159
0
                cpu_arithmetic_intensity: cpu_ai,
160
0
                gpu_arithmetic_intensity: gpu_ai,
161
0
            },
162
            // PMAT-452: Default byte budget based on memory bandwidth
163
0
            byte_budget: Some(crate::brick::ByteBudget::from_throughput(byte_budget_throughput)),
164
        }
165
0
    }
166
167
    /// Load from TOML file or detect if missing
168
0
    pub fn load_or_detect(path: &Path) -> Self {
169
0
        if path.exists() {
170
0
            if let Ok(content) = fs::read_to_string(path) {
171
0
                if let Ok(cap) = toml::from_str(&content) {
172
0
                    return cap;
173
0
                }
174
0
            }
175
0
        }
176
0
        let cap = Self::detect();
177
        // Try to cache it
178
0
        let _ = cap.save(path);
179
0
        cap
180
0
    }
181
182
    /// Save to TOML file
183
0
    pub fn save(&self, path: &Path) -> std::io::Result<()> {
184
0
        if let Some(parent) = path.parent() {
185
0
            fs::create_dir_all(parent)?;
186
0
        }
187
0
        let content = toml::to_string_pretty(self)
188
0
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
189
0
        fs::write(path, content)
190
0
    }
191
192
    /// Get the best available backend for a workload
193
0
    pub fn best_backend(&self) -> GpuBackend {
194
0
        self.gpu
195
0
            .as_ref()
196
0
            .map(|g| g.backend)
197
0
            .unwrap_or(GpuBackend::None)
198
0
    }
199
200
    /// Calculate expected throughput for a brick given its arithmetic intensity
201
0
    pub fn expected_throughput_gflops(&self, arithmetic_intensity: f64, use_gpu: bool) -> f64 {
202
0
        if use_gpu {
203
0
            if let Some(gpu) = &self.gpu {
204
0
                let memory_bound = gpu.memory_bw_gbps * arithmetic_intensity;
205
0
                let compute_bound = gpu.peak_tflops_fp32 * 1000.0;
206
0
                memory_bound.min(compute_bound)
207
            } else {
208
0
                self.cpu_expected_throughput(arithmetic_intensity)
209
            }
210
        } else {
211
0
            self.cpu_expected_throughput(arithmetic_intensity)
212
        }
213
0
    }
214
215
0
    fn cpu_expected_throughput(&self, arithmetic_intensity: f64) -> f64 {
216
0
        let memory_bound = self.cpu.memory_bw_gbps * arithmetic_intensity;
217
0
        let compute_bound = self.cpu.peak_gflops;
218
0
        memory_bound.min(compute_bound)
219
0
    }
220
221
    /// Determine if workload is memory-bound or compute-bound
222
0
    pub fn bottleneck(&self, arithmetic_intensity: f64, use_gpu: bool) -> Bottleneck {
223
0
        let threshold = if use_gpu {
224
0
            self.roofline.gpu_arithmetic_intensity.unwrap_or(f64::MAX)
225
        } else {
226
0
            self.roofline.cpu_arithmetic_intensity
227
        };
228
229
0
        if arithmetic_intensity < threshold {
230
0
            Bottleneck::Memory
231
        } else {
232
0
            Bottleneck::Compute
233
        }
234
0
    }
235
}
236
237
/// Workload bottleneck classification
238
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
239
pub enum Bottleneck {
240
    /// Limited by memory bandwidth
241
    Memory,
242
    /// Limited by compute throughput
243
    Compute,
244
}
245
246
/// Detect CPU capabilities
247
0
fn detect_cpu() -> CpuCapability {
248
0
    let simd = detect_simd();
249
0
    let cores = num_cpus::get_physical();
250
0
    let threads = num_cpus::get();
251
252
    // Estimate frequency (fallback to 3.0 GHz if unknown)
253
0
    let base_freq_ghz = 3.0;
254
255
    // Calculate peak GFLOP/s: cores × lanes × 2 (FMA) × freq
256
0
    let peak_gflops = (cores as f64) * (simd.lanes() as f64) * 2.0 * base_freq_ghz;
257
258
    // Estimate memory bandwidth (DDR5-5600 dual channel ≈ 89.6 GB/s)
259
0
    let memory_bw_gbps = 80.0; // Conservative estimate
260
261
0
    CpuCapability {
262
0
        vendor: "Unknown".to_string(),
263
0
        model: "Unknown".to_string(),
264
0
        cores,
265
0
        threads,
266
0
        simd,
267
0
        base_freq_ghz,
268
0
        peak_gflops,
269
0
        memory_bw_gbps,
270
0
    }
271
0
}
272
273
/// Detect best available SIMD width
274
0
fn detect_simd() -> SimdWidth {
275
    #[cfg(target_arch = "x86_64")]
276
    {
277
0
        if is_x86_feature_detected!("avx512f") {
278
0
            return SimdWidth::Avx512;
279
0
        }
280
0
        if is_x86_feature_detected!("avx2") {
281
0
            return SimdWidth::Avx2;
282
0
        }
283
0
        if is_x86_feature_detected!("sse2") {
284
0
            return SimdWidth::Sse2;
285
0
        }
286
    }
287
288
    #[cfg(target_arch = "aarch64")]
289
    {
290
        // NEON is always available on aarch64
291
        return SimdWidth::Neon128;
292
    }
293
294
    #[cfg(target_arch = "wasm32")]
295
    {
296
        return SimdWidth::WasmSimd128;
297
    }
298
299
0
    SimdWidth::Scalar
300
0
}
301
302
/// Detect GPU capabilities
303
0
fn detect_gpu() -> Option<GpuCapability> {
304
    // Check for CUDA first (highest performance)
305
    #[cfg(feature = "cuda")]
306
    {
307
        if let Some(gpu) = detect_cuda_gpu() {
308
            return Some(gpu);
309
        }
310
    }
311
312
    // Fallback: no GPU detected
313
0
    None
314
0
}
315
316
#[cfg(feature = "cuda")]
317
fn detect_cuda_gpu() -> Option<GpuCapability> {
318
    // This would use cuDeviceGetAttribute in a real implementation
319
    // For now, return None and let the caller provide GPU info
320
    None
321
}
322
323
/// Default hardware.toml path
324
0
pub fn default_hardware_path() -> std::path::PathBuf {
325
    #[cfg(feature = "hardware-detect")]
326
    {
327
        dirs::home_dir()
328
            .unwrap_or_else(|| std::path::PathBuf::from("."))
329
            .join(".pmat")
330
            .join("hardware.toml")
331
    }
332
    #[cfg(not(feature = "hardware-detect"))]
333
    {
334
0
        std::path::PathBuf::from(".pmat").join("hardware.toml")
335
    }
336
0
}
337
338
#[cfg(test)]
339
mod tests {
340
    use super::*;
341
342
    #[test]
343
    fn test_simd_detection() {
344
        let simd = detect_simd();
345
        // Should detect at least scalar
346
        assert!(simd.lanes() >= 1);
347
    }
348
349
    #[test]
350
    fn test_simd_lanes() {
351
        assert_eq!(SimdWidth::Scalar.lanes(), 1);
352
        assert_eq!(SimdWidth::Avx2.lanes(), 8);
353
        assert_eq!(SimdWidth::Avx512.lanes(), 16);
354
    }
355
356
    #[test]
357
    fn test_hardware_detection() {
358
        let cap = HardwareCapability::detect();
359
        assert!(cap.cpu.cores > 0);
360
        assert!(cap.cpu.peak_gflops > 0.0);
361
    }
362
363
    #[test]
364
    fn test_bottleneck_classification() {
365
        let cap = HardwareCapability::detect();
366
367
        // Low arithmetic intensity = memory bound
368
        assert_eq!(cap.bottleneck(0.1, false), Bottleneck::Memory);
369
370
        // High arithmetic intensity = compute bound
371
        assert_eq!(cap.bottleneck(1000.0, false), Bottleneck::Compute);
372
    }
373
374
    #[test]
375
    fn test_roofline_throughput() {
376
        let cap = HardwareCapability::detect();
377
378
        // Memory-bound workload
379
        let low_ai = cap.expected_throughput_gflops(0.1, false);
380
        // Compute-bound workload
381
        let high_ai = cap.expected_throughput_gflops(1000.0, false);
382
383
        // Low AI should give lower throughput (memory limited)
384
        assert!(low_ai < high_ai);
385
    }
386
387
    #[test]
388
    fn test_toml_roundtrip() {
389
        let cap = HardwareCapability::detect();
390
        let toml_str = toml::to_string_pretty(&cap).unwrap();
391
        let parsed: HardwareCapability = toml::from_str(&toml_str).unwrap();
392
        assert_eq!(cap.cpu.cores, parsed.cpu.cores);
393
    }
394
395
    #[test]
396
    fn test_byte_budget_in_hardware_toml() {
397
        let cap = HardwareCapability::detect();
398
399
        // Should have byte_budget populated
400
        assert!(cap.byte_budget.is_some());
401
        let budget = cap.byte_budget.unwrap();
402
        assert!(budget.gb_per_sec > 0.0);
403
        assert!(budget.gb_per_sec <= 25.0); // Capped at zstd max
404
405
        // Test TOML roundtrip preserves byte_budget
406
        let toml_str = toml::to_string_pretty(&cap).unwrap();
407
        assert!(toml_str.contains("[byte_budget]"));
408
        assert!(toml_str.contains("gb_per_sec"));
409
410
        let parsed: HardwareCapability = toml::from_str(&toml_str).unwrap();
411
        assert!(parsed.byte_budget.is_some());
412
        let parsed_budget = parsed.byte_budget.unwrap();
413
        assert!((parsed_budget.gb_per_sec - budget.gb_per_sec).abs() < 0.001);
414
    }
415
416
    #[test]
417
    fn test_byte_budget_backward_compat() {
418
        // Test that hardware.toml without byte_budget still parses
419
        let toml_without_budget = r#"
420
timestamp = "2026-01-13T18:00:00Z"
421
hostname = "test"
422
423
[cpu]
424
vendor = "Intel"
425
model = "Test CPU"
426
cores = 4
427
threads = 8
428
simd = "Avx2"
429
base_freq_ghz = 3.5
430
peak_gflops = 112.0
431
memory_bw_gbps = 80.0
432
433
[roofline]
434
cpu_arithmetic_intensity = 1.4
435
"#;
436
        let parsed: HardwareCapability = toml::from_str(toml_without_budget).unwrap();
437
        // byte_budget should be None when not in TOML (backward compat)
438
        assert!(parsed.byte_budget.is_none());
439
    }
440
441
    // Additional coverage tests
442
443
    #[test]
444
    fn test_simd_width_bits() {
445
        assert_eq!(SimdWidth::Scalar.bits(), 32);
446
        assert_eq!(SimdWidth::Sse2.bits(), 128);
447
        assert_eq!(SimdWidth::Neon128.bits(), 128);
448
        assert_eq!(SimdWidth::WasmSimd128.bits(), 128);
449
        assert_eq!(SimdWidth::Avx2.bits(), 256);
450
        assert_eq!(SimdWidth::Avx512.bits(), 512);
451
    }
452
453
    #[test]
454
    fn test_simd_width_compute_speedup() {
455
        assert!((SimdWidth::Scalar.compute_speedup() - 1.0).abs() < 0.01);
456
        assert!((SimdWidth::Sse2.compute_speedup() - 4.0).abs() < 0.01);
457
        assert!((SimdWidth::Neon128.compute_speedup() - 4.0).abs() < 0.01);
458
        assert!((SimdWidth::WasmSimd128.compute_speedup() - 4.0).abs() < 0.01);
459
        assert!((SimdWidth::Avx2.compute_speedup() - 10.0).abs() < 0.01);
460
        assert!((SimdWidth::Avx512.compute_speedup() - 12.0).abs() < 0.01);
461
    }
462
463
    #[test]
464
    fn test_best_backend() {
465
        let cap = HardwareCapability::detect();
466
        let backend = cap.best_backend();
467
        // Should be a valid backend (even if None)
468
        assert!(matches!(
469
            backend,
470
            GpuBackend::None | GpuBackend::Cuda | GpuBackend::Metal | GpuBackend::Vulkan | GpuBackend::Wgpu
471
        ));
472
    }
473
474
    #[test]
475
    fn test_load_or_detect_creates_new() {
476
        use std::path::PathBuf;
477
        let tmp_path = PathBuf::from("/tmp/trueno_test_nonexistent_12345.toml");
478
        // Ensure it doesn't exist
479
        let _ = std::fs::remove_file(&tmp_path);
480
481
        let cap = HardwareCapability::load_or_detect(&tmp_path);
482
        assert!(cap.cpu.cores > 0);
483
484
        // Cleanup
485
        let _ = std::fs::remove_file(&tmp_path);
486
    }
487
488
    #[test]
489
    fn test_save_and_load() {
490
        use std::path::PathBuf;
491
        let tmp_path = PathBuf::from("/tmp/trueno_test_save_load.toml");
492
493
        let original = HardwareCapability::detect();
494
        original.save(&tmp_path).expect("Failed to save");
495
496
        let loaded = HardwareCapability::load_or_detect(&tmp_path);
497
        assert_eq!(original.cpu.cores, loaded.cpu.cores);
498
        assert_eq!(original.hostname, loaded.hostname);
499
500
        // Cleanup
501
        let _ = std::fs::remove_file(&tmp_path);
502
    }
503
504
    #[test]
505
    fn test_expected_throughput_with_gpu() {
506
        // Create a capability with GPU
507
        let cap = HardwareCapability::detect();
508
509
        // Test GPU branch if available
510
        if cap.gpu.is_some() {
511
            let throughput_gpu = cap.expected_throughput_gflops(10.0, true);
512
            let throughput_cpu = cap.expected_throughput_gflops(10.0, false);
513
            // Both should be positive
514
            assert!(throughput_gpu > 0.0);
515
            assert!(throughput_cpu > 0.0);
516
        }
517
518
        // CPU path should always work
519
        let cpu_throughput = cap.expected_throughput_gflops(10.0, false);
520
        assert!(cpu_throughput > 0.0);
521
    }
522
523
    #[test]
524
    fn test_expected_throughput_with_fake_gpu() {
525
        // Create a capability with a fake GPU to test GPU branch
526
        let mut cap = HardwareCapability::detect();
527
        cap.gpu = Some(GpuCapability {
528
            vendor: "Test".to_string(),
529
            model: "Fake GPU".to_string(),
530
            backend: GpuBackend::Cuda,
531
            compute_capability: Some("8.9".to_string()),
532
            peak_tflops_fp32: 100.0,
533
            peak_tflops_tensor: Some(400.0),
534
            memory_bw_gbps: 1000.0,
535
            vram_gb: 24.0,
536
        });
537
538
        // This should exercise the GPU branch
539
        let throughput_gpu = cap.expected_throughput_gflops(10.0, true);
540
        let throughput_cpu = cap.expected_throughput_gflops(10.0, false);
541
542
        // GPU with 1000 GB/s and AI=10: memory_bound = 10000 GFLOPS
543
        // GPU compute = 100 TFLOPS = 100000 GFLOPS
544
        // Result should be min(10000, 100000) = 10000
545
        assert!((throughput_gpu - 10000.0).abs() < 1.0);
546
        assert!(throughput_cpu > 0.0);
547
    }
548
549
    #[test]
550
    fn test_load_invalid_toml() {
551
        use std::path::PathBuf;
552
        let tmp_path = PathBuf::from("/tmp/trueno_test_invalid.toml");
553
554
        // Write invalid TOML
555
        std::fs::write(&tmp_path, "this is not valid toml [[[").expect("Failed to write");
556
557
        // Should fall back to detect
558
        let cap = HardwareCapability::load_or_detect(&tmp_path);
559
        assert!(cap.cpu.cores > 0);
560
561
        // Cleanup
562
        let _ = std::fs::remove_file(&tmp_path);
563
    }
564
565
    #[test]
566
    fn test_expected_throughput_no_gpu_fallback() {
567
        // Create a capability without GPU
568
        let mut cap = HardwareCapability::detect();
569
        cap.gpu = None;
570
571
        // Request GPU but none available - should fallback to CPU
572
        let throughput_gpu_request = cap.expected_throughput_gflops(10.0, true);
573
        let throughput_cpu = cap.expected_throughput_gflops(10.0, false);
574
575
        // Both should give the same result since there's no GPU
576
        assert!((throughput_gpu_request - throughput_cpu).abs() < 0.001);
577
        assert!(throughput_cpu > 0.0);
578
    }
579
580
    #[test]
581
    fn test_bottleneck_with_gpu() {
582
        // Create capability with fake GPU to test GPU bottleneck path
583
        let mut cap = HardwareCapability::detect();
584
        cap.gpu = Some(GpuCapability {
585
            vendor: "Test".to_string(),
586
            model: "Fake GPU".to_string(),
587
            backend: GpuBackend::Cuda,
588
            compute_capability: Some("8.9".to_string()),
589
            peak_tflops_fp32: 100.0,
590
            peak_tflops_tensor: Some(400.0),
591
            memory_bw_gbps: 1000.0,
592
            vram_gb: 24.0,
593
        });
594
        cap.roofline.gpu_arithmetic_intensity = Some(50.0);
595
596
        // Low AI should be memory bound
597
        assert_eq!(cap.bottleneck(10.0, true), Bottleneck::Memory);
598
599
        // High AI should be compute bound
600
        assert_eq!(cap.bottleneck(100.0, true), Bottleneck::Compute);
601
602
        // Test edge case at threshold
603
        assert_eq!(cap.bottleneck(50.0, true), Bottleneck::Compute);
604
    }
605
606
    #[test]
607
    fn test_bottleneck_gpu_without_ai() {
608
        // Create capability with GPU but no gpu_arithmetic_intensity
609
        let mut cap = HardwareCapability::detect();
610
        cap.gpu = Some(GpuCapability {
611
            vendor: "Test".to_string(),
612
            model: "Fake GPU".to_string(),
613
            backend: GpuBackend::Cuda,
614
            compute_capability: None,
615
            peak_tflops_fp32: 50.0,
616
            peak_tflops_tensor: None,
617
            memory_bw_gbps: 500.0,
618
            vram_gb: 8.0,
619
        });
620
        cap.roofline.gpu_arithmetic_intensity = None; // No GPU AI set
621
622
        // When gpu_arithmetic_intensity is None, uses f64::MAX as threshold
623
        // So any finite AI should be memory bound
624
        assert_eq!(cap.bottleneck(1000.0, true), Bottleneck::Memory);
625
    }
626
627
    #[test]
628
    fn test_simd_width_neon() {
629
        // Test NEON SIMD width (4 lanes)
630
        assert_eq!(SimdWidth::Neon128.lanes(), 4);
631
    }
632
633
    #[test]
634
    fn test_simd_width_sse2() {
635
        // Test SSE2 SIMD width (4 lanes)
636
        assert_eq!(SimdWidth::Sse2.lanes(), 4);
637
    }
638
639
    #[test]
640
    fn test_best_backend_without_gpu() {
641
        let mut cap = HardwareCapability::detect();
642
        cap.gpu = None;
643
644
        // Should return None backend when no GPU
645
        assert_eq!(cap.best_backend(), GpuBackend::None);
646
    }
647
648
    #[test]
649
    fn test_best_backend_with_gpu() {
650
        let mut cap = HardwareCapability::detect();
651
        cap.gpu = Some(GpuCapability {
652
            vendor: "NVIDIA".to_string(),
653
            model: "RTX 4090".to_string(),
654
            backend: GpuBackend::Cuda,
655
            compute_capability: Some("8.9".to_string()),
656
            peak_tflops_fp32: 82.58,
657
            peak_tflops_tensor: Some(330.3),
658
            memory_bw_gbps: 1008.0,
659
            vram_gb: 24.0,
660
        });
661
662
        assert_eq!(cap.best_backend(), GpuBackend::Cuda);
663
    }
664
665
    #[test]
666
    fn test_gpu_backend_variants() {
667
        // Test all GPU backend variants
668
        assert_ne!(GpuBackend::None, GpuBackend::Cuda);
669
        assert_ne!(GpuBackend::Cuda, GpuBackend::Vulkan);
670
        assert_ne!(GpuBackend::Vulkan, GpuBackend::Metal);
671
672
        // Test debug formatting
673
        let debug_str = format!("{:?}", GpuBackend::Cuda);
674
        assert!(debug_str.contains("Cuda"));
675
    }
676
}