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