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
}