Skip to main content

trueno/brick/simd_config/
mod.rs

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
6use 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)]
17pub 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)]
33pub 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)]
46pub 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
57impl LazySimdConfig {
58    /// Create new lazy config, detecting best backend.
59    #[must_use]
60    pub fn new() -> Self {
61        Self {
62            state: SimdBackendState::Uninitialized,
63            best_backend: Self::detect_best_backend(),
64            amx_supported: Self::detect_amx(),
65            tile_config: None,
66        }
67    }
68
69    /// Detect best available SIMD backend.
70    fn detect_best_backend() -> ComputeBackend {
71        #[cfg(target_arch = "x86_64")]
72        {
73            if is_x86_feature_detected!("avx512f") {
74                return ComputeBackend::Avx512;
75            }
76            if is_x86_feature_detected!("avx2") {
77                return ComputeBackend::Avx2;
78            }
79            if is_x86_feature_detected!("sse2") {
80                return ComputeBackend::Sse2;
81            }
82        }
83        #[cfg(target_arch = "aarch64")]
84        {
85            // NEON is always available on aarch64
86            ComputeBackend::Neon
87        }
88        #[cfg(not(target_arch = "aarch64"))]
89        {
90            ComputeBackend::Scalar
91        }
92    }
93
94    /// Detect AMX support (Intel Sapphire Rapids+).
95    fn detect_amx() -> bool {
96        #[cfg(target_arch = "x86_64")]
97        {
98            // AMX requires specific CPUID checks
99            // For now, return false as AMX is rare
100            false
101        }
102        #[cfg(not(target_arch = "x86_64"))]
103        {
104            false
105        }
106    }
107
108    /// Ensure SIMD is configured, initializing lazily if needed.
109    pub fn ensure_ready(&mut self) -> Result<ComputeBackend, SimdBackendState> {
110        match self.state {
111            SimdBackendState::Ready => Ok(self.best_backend),
112            SimdBackendState::Failed => Err(SimdBackendState::Failed),
113            SimdBackendState::Configuring => Err(SimdBackendState::Configuring),
114            SimdBackendState::Uninitialized => {
115                self.state = SimdBackendState::Configuring;
116
117                // Configure AMX tiles if supported
118                if self.amx_supported {
119                    self.tile_config = Some(AmxTileConfig {
120                        palette: 1,
121                        start_row: 0,
122                        rows: 16,
123                        bytes_per_row: 64,
124                    });
125                    // In real implementation, would call LDTILECFG here
126                }
127
128                self.state = SimdBackendState::Ready;
129                Ok(self.best_backend)
130            }
131        }
132    }
133
134    /// Get current state.
135    #[must_use]
136    pub fn state(&self) -> SimdBackendState {
137        self.state
138    }
139
140    /// Get best backend without initializing.
141    #[must_use]
142    pub fn best_backend(&self) -> ComputeBackend {
143        self.best_backend
144    }
145
146    /// Check if AMX is supported.
147    #[must_use]
148    pub fn has_amx(&self) -> bool {
149        self.amx_supported
150    }
151
152    /// Reset to uninitialized state.
153    pub fn reset(&mut self) {
154        self.state = SimdBackendState::Uninitialized;
155        self.tile_config = None;
156    }
157}
158
159impl Default for LazySimdConfig {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165// ----------------------------------------------------------------------------
166// LCP-13: Unroll-and-Tail Vectorization
167// ----------------------------------------------------------------------------
168
169/// Unroll factor for SIMD loops.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum UnrollFactor {
172    /// No unrolling (1x)
173    None,
174    /// 2x unroll
175    X2,
176    /// 4x unroll
177    X4,
178    /// 8x unroll (AVX-512)
179    X8,
180}
181
182impl UnrollFactor {
183    /// Get numeric factor.
184    #[must_use]
185    pub fn value(&self) -> usize {
186        match self {
187            UnrollFactor::None => 1,
188            UnrollFactor::X2 => 2,
189            UnrollFactor::X4 => 4,
190            UnrollFactor::X8 => 8,
191        }
192    }
193
194    /// Get optimal factor for backend.
195    #[must_use]
196    pub fn for_backend(backend: ComputeBackend) -> Self {
197        match backend {
198            ComputeBackend::Avx512 => UnrollFactor::X8,
199            ComputeBackend::Avx2 => UnrollFactor::X4,
200            ComputeBackend::Sse2 | ComputeBackend::Neon => UnrollFactor::X2,
201            _ => UnrollFactor::None,
202        }
203    }
204}
205
206/// Helper for unroll-and-tail loop pattern.
207///
208/// Processes data in unrolled chunks, then handles the tail.
209#[derive(Debug)]
210pub struct UnrollTailIterator {
211    /// Total elements
212    total: usize,
213    /// Current position
214    position: usize,
215    /// Elements per unrolled iteration
216    chunk_size: usize,
217}
218
219impl UnrollTailIterator {
220    /// Create iterator for given size and unroll factor.
221    pub fn new(total: usize, factor: UnrollFactor) -> Self {
222        Self { total, position: 0, chunk_size: factor.value() }
223    }
224
225    /// Get number of full unrolled iterations.
226    #[must_use]
227    pub fn full_iterations(&self) -> usize {
228        self.total / self.chunk_size
229    }
230
231    /// Get tail size (remainder).
232    #[must_use]
233    pub fn tail_size(&self) -> usize {
234        self.total % self.chunk_size
235    }
236
237    /// Check if there's a tail to process.
238    #[must_use]
239    pub fn has_tail(&self) -> bool {
240        self.tail_size() > 0
241    }
242
243    /// Get next chunk range for unrolled iteration.
244    pub fn next_chunk(&mut self) -> Option<(usize, usize)> {
245        if self.position + self.chunk_size <= self.total {
246            let start = self.position;
247            self.position += self.chunk_size;
248            Some((start, start + self.chunk_size))
249        } else {
250            None
251        }
252    }
253
254    /// Get tail range (call after all chunks consumed).
255    pub fn tail_range(&self) -> Option<(usize, usize)> {
256        let tail_start = self.full_iterations() * self.chunk_size;
257        if tail_start < self.total {
258            Some((tail_start, self.total))
259        } else {
260            None
261        }
262    }
263}
264
265/// Process a slice with unroll-and-tail pattern.
266///
267/// # Example
268/// ```ignore
269/// let result = unroll_tail_process(
270///     &data,
271///     UnrollFactor::X4,
272///     |chunk| chunk.iter().sum::<f32>(), // Unrolled body
273///     |elem| *elem,                       // Tail body
274/// );
275/// ```
276pub fn unroll_tail_process<T, U, F, G>(
277    data: &[T],
278    factor: UnrollFactor,
279    mut process_chunk: F,
280    mut process_elem: G,
281) -> Vec<U>
282where
283    F: FnMut(&[T]) -> U,
284    G: FnMut(&T) -> U,
285{
286    let mut iter = UnrollTailIterator::new(data.len(), factor);
287    let mut results =
288        Vec::with_capacity(iter.full_iterations() + if iter.has_tail() { 1 } else { 0 });
289
290    // Process full chunks
291    while let Some((start, end)) = iter.next_chunk() {
292        results.push(process_chunk(&data[start..end]));
293    }
294
295    // Process tail
296    if let Some((start, end)) = iter.tail_range() {
297        for elem in &data[start..end] {
298            results.push(process_elem(elem));
299        }
300    }
301
302    results
303}
304
305#[cfg(test)]
306mod tests;