## 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/brick/simd_config.rs

    Line
    Count
    Source
    1
    //! SIMD Configuration and Lazy Initialization
    2
    //!
    3
    //! LCP-07: Lazy AMX/SIMD tile configuration for expensive state setup.
    4
    //! LCP-13: Unroll-and-tail vectorization patterns.
    5
    6
    use super::ComputeBackend;
    7
    8
    // ----------------------------------------------------------------------------
    9
    // LCP-07: Lazy AMX Tile Config
    10
    // ----------------------------------------------------------------------------
    11
    12
    /// SIMD backend state for lazy initialization.
    13
    ///
    14
    /// AMX (Advanced Matrix Extensions) and AVX-512 require tile configuration
    15
    /// that's expensive to set up. This tracks whether initialization has occurred.
    16
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
    17
    pub enum SimdBackendState {
    18
        /// Not initialized - will configure on first use
    19
        #[default]
    20
        Uninitialized,
    21
        /// Configuration in progress
    22
        Configuring,
    23
        /// Ready to use
    24
        Ready,
    25
        /// Failed to initialize (fallback to scalar)
    26
        Failed,
    27
    }
    28
    29
    /// Lazy SIMD tile configuration manager.
    30
    ///
    31
    /// Defers expensive SIMD state setup until actually needed.
    32
    #[derive(Debug)]
    33
    pub struct LazySimdConfig {
    34
        /// Current state
    35
        state: SimdBackendState,
    36
        /// Best available backend
    37
        best_backend: ComputeBackend,
    38
        /// Whether AMX is supported
    39
        amx_supported: bool,
    40
        /// Tile configuration (for AMX)
    41
        tile_config: Option<AmxTileConfig>,
    42
    }
    43
    44
    /// AMX tile configuration (8x8 tile palette).
    45
    #[derive(Debug, Clone, Copy, Default)]
    46
    pub struct AmxTileConfig {
    47
        /// Palette ID (0-1)
    48
        pub palette: u8,
    49
        /// Start row
    50
        pub start_row: u8,
    51
        /// Number of rows per tile
    52
        pub rows: u8,
    53
        /// Bytes per row
    54
        pub bytes_per_row: u16,
    55
    }
    56
    57
    impl LazySimdConfig {
    58
        /// Create new lazy config, detecting best backend.
    59
        #[must_use]
    60
    0
        pub fn new() -> Self {
    61
    0
            Self {
    62
    0
                state: SimdBackendState::Uninitialized,
    63
    0
                best_backend: Self::detect_best_backend(),
    64
    0
                amx_supported: Self::detect_amx(),
    65
    0
                tile_config: None,
    66
    0
            }
    67
    0
        }
    68
    69
        /// Detect best available SIMD backend.
    70
    0
        fn detect_best_backend() -> ComputeBackend {
    71
            #[cfg(target_arch = "x86_64")]
    72
            {
    73
    0
                if is_x86_feature_detected!("avx512f") {
    74
    0
                    return ComputeBackend::Avx512;
    75
    0
                }
    76
    0
                if is_x86_feature_detected!("avx2") {
    77
    0
                    return ComputeBackend::Avx2;
    78
    0
                }
    79
    0
                if is_x86_feature_detected!("sse2") {
    80
    0
                    return ComputeBackend::Sse2;
    81
    0
                }
    82
            }
    83
            #[cfg(target_arch = "aarch64")]
    84
            {
    85
                // NEON is always available on aarch64
    86
                return ComputeBackend::Neon;
    87
            }
    88
    0
            ComputeBackend::Scalar
    89
    0
        }
    90
    91
        /// Detect AMX support (Intel Sapphire Rapids+).
    92
    0
        fn detect_amx() -> bool {
    93
            #[cfg(target_arch = "x86_64")]
    94
            {
    95
                // AMX requires specific CPUID checks
    96
                // For now, return false as AMX is rare
    97
    0
                false
    98
            }
    99
            #[cfg(not(target_arch = "x86_64"))]
    100
            {
    101
                false
    102
            }
    103
    0
        }
    104
    105
        /// Ensure SIMD is configured, initializing lazily if needed.
    106
    0
        pub fn ensure_ready(&mut self) -> Result<ComputeBackend, SimdBackendState> {
    107
    0
            match self.state {
    108
    0
                SimdBackendState::Ready => Ok(self.best_backend),
    109
    0
                SimdBackendState::Failed => Err(SimdBackendState::Failed),
    110
    0
                SimdBackendState::Configuring => Err(SimdBackendState::Configuring),
    111
                SimdBackendState::Uninitialized => {
    112
    0
                    self.state = SimdBackendState::Configuring;
    113
    114
                    // Configure AMX tiles if supported
    115
    0
                    if self.amx_supported {
    116
    0
                        self.tile_config = Some(AmxTileConfig {
    117
    0
                            palette: 1,
    118
    0
                            start_row: 0,
    119
    0
                            rows: 16,
    120
    0
                            bytes_per_row: 64,
    121
    0
                        });
    122
    0
                        // In real implementation, would call LDTILECFG here
    123
    0
                    }
    124
    125
    0
                    self.state = SimdBackendState::Ready;
    126
    0
                    Ok(self.best_backend)
    127
                }
    128
            }
    129
    0
        }
    130
    131
        /// Get current state.
    132
        #[must_use]
    133
    0
        pub fn state(&self) -> SimdBackendState {
    134
    0
            self.state
    135
    0
        }
    136
    137
        /// Get best backend without initializing.
    138
        #[must_use]
    139
    0
        pub fn best_backend(&self) -> ComputeBackend {
    140
    0
            self.best_backend
    141
    0
        }
    142
    143
        /// Check if AMX is supported.
    144
        #[must_use]
    145
    0
        pub fn has_amx(&self) -> bool {
    146
    0
            self.amx_supported
    147
    0
        }
    148
    149
        /// Reset to uninitialized state.
    150
    0
        pub fn reset(&mut self) {
    151
    0
            self.state = SimdBackendState::Uninitialized;
    152
    0
            self.tile_config = None;
    153
    0
        }
    154
    }
    155
    156
    impl Default for LazySimdConfig {
    157
    0
        fn default() -> Self {
    158
    0
            Self::new()
    159
    0
        }
    160
    }
    161
    162
    // ----------------------------------------------------------------------------
    163
    // LCP-13: Unroll-and-Tail Vectorization
    164
    // ----------------------------------------------------------------------------
    165
    166
    /// Unroll factor for SIMD loops.
    167
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    168
    pub enum UnrollFactor {
    169
        /// No unrolling (1x)
    170
        None,
    171
        /// 2x unroll
    172
        X2,
    173
        /// 4x unroll
    174
        X4,
    175
        /// 8x unroll (AVX-512)
    176
        X8,
    177
    }
    178
    179
    impl UnrollFactor {
    180
        /// Get numeric factor.
    181
        #[must_use]
    182
    0
        pub fn value(&self) -> usize {
    183
    0
            match self {
    184
    0
                UnrollFactor::None => 1,
    185
    0
                UnrollFactor::X2 => 2,
    186
    0
                UnrollFactor::X4 => 4,
    187
    0
                UnrollFactor::X8 => 8,
    188
            }
    189
    0
        }
    190
    191
        /// Get optimal factor for backend.
    192
        #[must_use]
    193
    0
        pub fn for_backend(backend: ComputeBackend) -> Self {
    194
    0
            match backend {
    195
    0
                ComputeBackend::Avx512 => UnrollFactor::X8,
    196
    0
                ComputeBackend::Avx2 => UnrollFactor::X4,
    197
    0
                ComputeBackend::Sse2 | ComputeBackend::Neon => UnrollFactor::X2,
    198
    0
                _ => UnrollFactor::None,
    199
            }
    200
    0
        }
    201
    }
    202
    203
    /// Helper for unroll-and-tail loop pattern.
    204
    ///
    205
    /// Processes data in unrolled chunks, then handles the tail.
    206
    #[derive(Debug)]
    207
    pub struct UnrollTailIterator {
    208
        /// Total elements
    209
        total: usize,
    210
        /// Current position
    211
        position: usize,
    212
        /// Elements per unrolled iteration
    213
        chunk_size: usize,
    214
    }
    215
    216
    impl UnrollTailIterator {
    217
        /// Create iterator for given size and unroll factor.
    218
    0
        pub fn new(total: usize, factor: UnrollFactor) -> Self {
    219
    0
            Self {
    220
    0
                total,
    221
    0
                position: 0,
    222
    0
                chunk_size: factor.value(),
    223
    0
            }
    224
    0
        }
    225
    226
        /// Get number of full unrolled iterations.
    227
        #[must_use]
    228
    0
        pub fn full_iterations(&self) -> usize {
    229
    0
            self.total / self.chunk_size
    230
    0
        }
    231
    232
        /// Get tail size (remainder).
    233
        #[must_use]
    234
    0
        pub fn tail_size(&self) -> usize {
    235
    0
            self.total % self.chunk_size
    236
    0
        }
    237
    238
        /// Check if there's a tail to process.
    239
        #[must_use]
    240
    0
        pub fn has_tail(&self) -> bool {
    241
    0
            self.tail_size() > 0
    242
    0
        }
    243
    244
        /// Get next chunk range for unrolled iteration.
    245
    0
        pub fn next_chunk(&mut self) -> Option<(usize, usize)> {
    246
    0
            if self.position + self.chunk_size <= self.total {
    247
    0
                let start = self.position;
    248
    0
                self.position += self.chunk_size;
    249
    0
                Some((start, start + self.chunk_size))
    250
            } else {
    251
    0
                None
    252
            }
    253
    0
        }
    254
    255
        /// Get tail range (call after all chunks consumed).
    256
    0
        pub fn tail_range(&self) -> Option<(usize, usize)> {
    257
    0
            let tail_start = self.full_iterations() * self.chunk_size;
    258
    0
            if tail_start < self.total {
    259
    0
                Some((tail_start, self.total))
    260
            } else {
    261
    0
                None
    262
            }
    263
    0
        }
    264
    }
    265
    266
    /// Process a slice with unroll-and-tail pattern.
    267
    ///
    268
    /// # Example
    269
    /// ```ignore
    270
    /// let result = unroll_tail_process(
    271
    ///     &data,
    272
    ///     UnrollFactor::X4,
    273
    ///     |chunk| chunk.iter().sum::<f32>(), // Unrolled body
    274
    ///     |elem| *elem,                       // Tail body
    275
    /// );
    276
    /// ```
    277
    0
    pub fn unroll_tail_process<T, U, F, G>(
    278
    0
        data: &[T],
    279
    0
        factor: UnrollFactor,
    280
    0
        mut process_chunk: F,
    281
    0
        mut process_elem: G,
    282
    0
    ) -> Vec<U>
    283
    0
    where
    284
    0
        F: FnMut(&[T]) -> U,
    285
    0
        G: FnMut(&T) -> U,
    286
    {
    287
    0
        let mut iter = UnrollTailIterator::new(data.len(), factor);
    288
    0
        let mut results =
    289
    0
            Vec::with_capacity(iter.full_iterations() + if iter.has_tail() { 1 } else { 0 });
    290
    291
        // Process full chunks
    292
    0
        while let Some((start, end)) = iter.next_chunk() {
    293
    0
            results.push(process_chunk(&data[start..end]));
    294
    0
        }
    295
    296
        // Process tail
    297
    0
        if let Some((start, end)) = iter.tail_range() {
    298
    0
            for elem in &data[start..end] {
    299
    0
                results.push(process_elem(elem));
    300
    0
            }
    301
    0
        }
    302
    303
    0
        results
    304
    0
    }
    305
    306
    #[cfg(test)]
    307
    mod tests {
    308
        use super::*;
    309
    310
        // =========================================================================
    311
        // SimdBackendState Tests
    312
        // =========================================================================
    313
    314
        #[test]
    315
        fn test_simd_backend_state_default() {
    316
            let state = SimdBackendState::default();
    317
            assert_eq!(state, SimdBackendState::Uninitialized);
    318
        }
    319
    320
        #[test]
    321
        fn test_simd_backend_state_variants() {
    322
            let states = [
    323
                SimdBackendState::Uninitialized,
    324
                SimdBackendState::Configuring,
    325
                SimdBackendState::Ready,
    326
                SimdBackendState::Failed,
    327
            ];
    328
            // All variants are distinct
    329
            for i in 0..states.len() {
    330
                for j in (i + 1)..states.len() {
    331
                    assert_ne!(states[i], states[j]);
    332
                }
    333
            }
    334
        }
    335
    336
        // =========================================================================
    337
        // LazySimdConfig Tests
    338
        // =========================================================================
    339
    340
        #[test]
    341
        fn test_lazy_simd_config_new() {
    342
            let config = LazySimdConfig::new();
    343
            assert_eq!(config.state(), SimdBackendState::Uninitialized);
    344
        }
    345
    346
        #[test]
    347
        fn test_lazy_simd_config_best_backend() {
    348
            let config = LazySimdConfig::new();
    349
            // Best backend should be detected and not crash
    350
            let _backend = config.best_backend();
    351
        }
    352
    353
        #[test]
    354
        fn test_lazy_simd_config_ensure_ready() {
    355
            let mut config = LazySimdConfig::new();
    356
            let result = config.ensure_ready();
    357
            assert!(result.is_ok());
    358
            assert_eq!(config.state(), SimdBackendState::Ready);
    359
        }
    360
    361
        #[test]
    362
        fn test_lazy_simd_config_ensure_ready_idempotent() {
    363
            let mut config = LazySimdConfig::new();
    364
    365
            let result1 = config.ensure_ready();
    366
            let result2 = config.ensure_ready();
    367
    368
            assert_eq!(result1, result2);
    369
        }
    370
    371
        #[test]
    372
        fn test_lazy_simd_config_reset() {
    373
            let mut config = LazySimdConfig::new();
    374
            let _ = config.ensure_ready();
    375
            assert_eq!(config.state(), SimdBackendState::Ready);
    376
    377
            config.reset();
    378
            assert_eq!(config.state(), SimdBackendState::Uninitialized);
    379
        }
    380
    381
        #[test]
    382
        fn test_lazy_simd_config_has_amx() {
    383
            let config = LazySimdConfig::new();
    384
            // AMX is typically not available, but this shouldn't crash
    385
            let _has_amx = config.has_amx();
    386
        }
    387
    388
        #[test]
    389
        fn test_lazy_simd_config_default() {
    390
            let config = LazySimdConfig::default();
    391
            assert_eq!(config.state(), SimdBackendState::Uninitialized);
    392
        }
    393
    394
        // =========================================================================
    395
        // AmxTileConfig Tests
    396
        // =========================================================================
    397
    398
        #[test]
    399
        fn test_amx_tile_config_default() {
    400
            let config = AmxTileConfig::default();
    401
            assert_eq!(config.palette, 0);
    402
            assert_eq!(config.start_row, 0);
    403
            assert_eq!(config.rows, 0);
    404
            assert_eq!(config.bytes_per_row, 0);
    405
        }
    406
    407
        #[test]
    408
        fn test_amx_tile_config_custom() {
    409
            let config = AmxTileConfig {
    410
                palette: 1,
    411
                start_row: 0,
    412
                rows: 16,
    413
                bytes_per_row: 64,
    414
            };
    415
            assert_eq!(config.palette, 1);
    416
            assert_eq!(config.rows, 16);
    417
            assert_eq!(config.bytes_per_row, 64);
    418
        }
    419
    420
        // =========================================================================
    421
        // UnrollFactor Tests
    422
        // =========================================================================
    423
    424
        #[test]
    425
        fn test_unroll_factor_value() {
    426
            assert_eq!(UnrollFactor::None.value(), 1);
    427
            assert_eq!(UnrollFactor::X2.value(), 2);
    428
            assert_eq!(UnrollFactor::X4.value(), 4);
    429
            assert_eq!(UnrollFactor::X8.value(), 8);
    430
        }
    431
    432
        #[test]
    433
        fn test_unroll_factor_for_backend() {
    434
            assert_eq!(UnrollFactor::for_backend(ComputeBackend::Avx512), UnrollFactor::X8);
    435
            assert_eq!(UnrollFactor::for_backend(ComputeBackend::Avx2), UnrollFactor::X4);
    436
            assert_eq!(UnrollFactor::for_backend(ComputeBackend::Sse2), UnrollFactor::X2);
    437
            assert_eq!(UnrollFactor::for_backend(ComputeBackend::Neon), UnrollFactor::X2);
    438
            assert_eq!(UnrollFactor::for_backend(ComputeBackend::Scalar), UnrollFactor::None);
    439
        }
    440
    441
        // =========================================================================
    442
        // UnrollTailIterator Tests
    443
        // =========================================================================
    444
    445
        #[test]
    446
        fn test_unroll_tail_iterator_basic() {
    447
            let iter = UnrollTailIterator::new(10, UnrollFactor::X4);
    448
            assert_eq!(iter.full_iterations(), 2); // 10 / 4 = 2
    449
            assert_eq!(iter.tail_size(), 2); // 10 % 4 = 2
    450
            assert!(iter.has_tail());
    451
        }
    452
    453
        #[test]
    454
        fn test_unroll_tail_iterator_no_tail() {
    455
            let iter = UnrollTailIterator::new(12, UnrollFactor::X4);
    456
            assert_eq!(iter.full_iterations(), 3);
    457
            assert_eq!(iter.tail_size(), 0);
    458
            assert!(!iter.has_tail());
    459
        }
    460
    461
        #[test]
    462
        fn test_unroll_tail_iterator_next_chunk() {
    463
            let mut iter = UnrollTailIterator::new(10, UnrollFactor::X4);
    464
    465
            assert_eq!(iter.next_chunk(), Some((0, 4)));
    466
            assert_eq!(iter.next_chunk(), Some((4, 8)));
    467
            assert_eq!(iter.next_chunk(), None); // No more full chunks
    468
        }
    469
    470
        #[test]
    471
        fn test_unroll_tail_iterator_tail_range() {
    472
            let iter = UnrollTailIterator::new(10, UnrollFactor::X4);
    473
            assert_eq!(iter.tail_range(), Some((8, 10)));
    474
        }
    475
    476
        #[test]
    477
        fn test_unroll_tail_iterator_tail_range_no_tail() {
    478
            let iter = UnrollTailIterator::new(8, UnrollFactor::X4);
    479
            assert_eq!(iter.tail_range(), None);
    480
        }
    481
    482
        #[test]
    483
        fn test_unroll_tail_iterator_empty() {
    484
            let iter = UnrollTailIterator::new(0, UnrollFactor::X4);
    485
            assert_eq!(iter.full_iterations(), 0);
    486
            assert_eq!(iter.tail_size(), 0);
    487
            assert!(!iter.has_tail());
    488
        }
    489
    490
        // =========================================================================
    491
        // unroll_tail_process Tests
    492
        // =========================================================================
    493
    494
        #[test]
    495
        fn test_unroll_tail_process_basic() {
    496
            let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
    497
            let results = unroll_tail_process(
    498
                &data,
    499
                UnrollFactor::X4,
    500
                |chunk| chunk.iter().sum::<f32>(),
    501
                |elem| *elem,
    502
            );
    503
    504
            // 2 full chunks: [1,2,3,4] -> 10.0, [5,6,7,8] -> 26.0
    505
            // Tail: 9.0, 10.0
    506
            assert_eq!(results.len(), 4);
    507
            assert_eq!(results[0], 10.0); // 1+2+3+4
    508
            assert_eq!(results[1], 26.0); // 5+6+7+8
    509
            assert_eq!(results[2], 9.0);
    510
            assert_eq!(results[3], 10.0);
    511
        }
    512
    513
        #[test]
    514
        fn test_unroll_tail_process_no_tail() {
    515
            let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
    516
            let results = unroll_tail_process(
    517
                &data,
    518
                UnrollFactor::X4,
    519
                |chunk| chunk.iter().sum::<i32>(),
    520
                |elem| *elem,
    521
            );
    522
    523
            assert_eq!(results.len(), 2);
    524
            assert_eq!(results[0], 10); // 1+2+3+4
    525
            assert_eq!(results[1], 26); // 5+6+7+8
    526
        }
    527
    528
        #[test]
    529
        fn test_unroll_tail_process_empty() {
    530
            let data: Vec<i32> = vec![];
    531
            let results = unroll_tail_process(&data, UnrollFactor::X4, |_| 0, |_| 0);
    532
            assert!(results.is_empty());
    533
        }
    534
    535
        #[test]
    536
        fn test_unroll_tail_process_small() {
    537
            let data = vec![1, 2, 3]; // Smaller than chunk size
    538
            let results = unroll_tail_process(&data, UnrollFactor::X4, |_| 0, |elem| *elem);
    539
    540
            // No full chunks, only tail
    541
            assert_eq!(results.len(), 3);
    542
            assert_eq!(results, vec![1, 2, 3]);
    543
        }
    544
    545
        // =========================================================================
    546
        // FALSIFICATION TESTS
    547
        // =========================================================================
    548
    549
        /// FALSIFICATION TEST: UnrollFactor values must be powers of 2
    550
        #[test]
    551
        fn test_falsify_unroll_factor_powers_of_two() {
    552
            let factors = [
    553
                UnrollFactor::None,
    554
                UnrollFactor::X2,
    555
                UnrollFactor::X4,
    556
                UnrollFactor::X8,
    557
            ];
    558
    559
            for factor in &factors {
    560
                let value = factor.value();
    561
                assert!(
    562
                    value.is_power_of_two(),
    563
                    "FALSIFICATION FAILED: UnrollFactor {:?} value {} is not power of 2",
    564
                    factor,
    565
                    value
    566
                );
    567
            }
    568
        }
    569
    570
        /// FALSIFICATION TEST: UnrollTailIterator must cover all elements exactly once
    571
        #[test]
    572
        fn test_falsify_unroll_tail_covers_all() {
    573
            for total in [1, 7, 8, 10, 100, 1000] {
    574
                for factor in [UnrollFactor::None, UnrollFactor::X2, UnrollFactor::X4, UnrollFactor::X8]
    575
                {
    576
                    let mut iter = UnrollTailIterator::new(total, factor);
    577
                    let mut covered = 0usize;
    578
    579
                    // Count elements in full chunks
    580
                    while let Some((start, end)) = iter.next_chunk() {
    581
                        covered += end - start;
    582
                    }
    583
    584
                    // Count elements in tail
    585
                    if let Some((start, end)) = iter.tail_range() {
    586
                        covered += end - start;
    587
                    }
    588
    589
                    assert_eq!(
    590
                        covered, total,
    591
                        "FALSIFICATION FAILED: UnrollTailIterator({}, {:?}) covered {} elements, expected {}",
    592
                        total, factor, covered, total
    593
                    );
    594
                }
    595
            }
    596
        }
    597
    598
        /// FALSIFICATION TEST: LazySimdConfig state transitions must be valid
    599
        #[test]
    600
        fn test_falsify_simd_config_state_transitions() {
    601
            let mut config = LazySimdConfig::new();
    602
    603
            // Initial state must be Uninitialized
    604
            assert_eq!(
    605
                config.state(),
    606
                SimdBackendState::Uninitialized,
    607
                "FALSIFICATION FAILED: Initial state should be Uninitialized"
    608
            );
    609
    610
            // After ensure_ready, state must be Ready
    611
            let result = config.ensure_ready();
    612
            assert!(
    613
                result.is_ok(),
    614
                "FALSIFICATION FAILED: ensure_ready should succeed"
    615
            );
    616
            assert_eq!(
    617
                config.state(),
    618
                SimdBackendState::Ready,
    619
                "FALSIFICATION FAILED: State should be Ready after ensure_ready"
    620
            );
    621
    622
            // After reset, state must be Uninitialized again
    623
            config.reset();
    624
            assert_eq!(
    625
                config.state(),
    626
                SimdBackendState::Uninitialized,
    627
                "FALSIFICATION FAILED: State should be Uninitialized after reset"
    628
            );
    629
        }
    630
    }